From c8e99801ac0aa037f31ab9b8e2a02f6fea4fd0ed Mon Sep 17 00:00:00 2001
From: Dilawar Singh <dilawars@ncbs.res.in>
Date: Sun, 26 Jun 2016 09:51:53 +0530
Subject: [PATCH] Squashed 'moose-gui/' changes from a3d3a06..bc510df

bc510df removal of whitespace in target Name
9eb3039 colormap check
6884e38 Files with no model will not open pyqt widget, atleast one empty compartment should exist
c830fa0 Merge pull request #9 from dilawar/master
e1efc3d Merge branch 'master' of https://github.com/BhallaLab/moose-gui
9603662 Added suds to the source code, removing a dependency.

git-subtree-dir: moose-gui
git-subtree-split: bc510df870a692283993da17d0fb5bef67edab25
---
 __init__.py                |    2 +-
 biomodelsclient.py         |    1 -
 mgui.py                    |   80 ++-
 mload.py                   |    4 +-
 plugins/kkitUtil.py        |    7 +-
 suds/__init__.py           |  192 +++++++
 suds/argparser.py          |  419 ++++++++++++++
 suds/bindings/__init__.py  |   18 +
 suds/bindings/binding.py   |  474 ++++++++++++++++
 suds/bindings/document.py  |  157 +++++
 suds/bindings/multiref.py  |  124 ++++
 suds/bindings/rpc.py       |   91 +++
 suds/builder.py            |  117 ++++
 suds/cache.py              |  301 ++++++++++
 suds/client.py             |  832 +++++++++++++++++++++++++++
 suds/metrics.py            |   63 ++
 suds/mx/__init__.py        |   59 ++
 suds/mx/appender.py        |  302 ++++++++++
 suds/mx/basic.py           |   45 ++
 suds/mx/core.py            |  154 +++++
 suds/mx/encoded.py         |  131 +++++
 suds/mx/literal.py         |  287 ++++++++++
 suds/mx/typer.py           |  119 ++++
 suds/options.py            |  150 +++++
 suds/plugin.py             |  257 +++++++++
 suds/properties.py         |  539 ++++++++++++++++++
 suds/reader.py             |  166 ++++++
 suds/resolver.py           |  493 ++++++++++++++++
 suds/sax/__init__.py       |  106 ++++
 suds/sax/attribute.py      |  174 ++++++
 suds/sax/date.py           |  458 +++++++++++++++
 suds/sax/document.py       |  176 ++++++
 suds/sax/element.py        | 1106 ++++++++++++++++++++++++++++++++++++
 suds/sax/enc.py            |   79 +++
 suds/sax/parser.py         |  136 +++++
 suds/sax/text.py           |  116 ++++
 suds/servicedefinition.py  |  240 ++++++++
 suds/serviceproxy.py       |   80 +++
 suds/soaparray.py          |   71 +++
 suds/store.py              |  596 +++++++++++++++++++
 suds/sudsobject.py         |  391 +++++++++++++
 suds/transport/__init__.py |  135 +++++
 suds/transport/http.py     |  254 +++++++++
 suds/transport/https.py    |   99 ++++
 suds/transport/options.py  |   58 ++
 suds/umx/__init__.py       |   56 ++
 suds/umx/attrlist.py       |   88 +++
 suds/umx/basic.py          |   41 ++
 suds/umx/core.py           |  214 +++++++
 suds/umx/encoded.py        |  126 ++++
 suds/umx/typed.py          |  140 +++++
 suds/version.py            |   26 +
 suds/wsdl.py               |  917 ++++++++++++++++++++++++++++++
 suds/wsse.py               |  212 +++++++
 suds/xsd/__init__.py       |   75 +++
 suds/xsd/deplist.py        |  140 +++++
 suds/xsd/doctor.py         |  223 ++++++++
 suds/xsd/query.py          |  208 +++++++
 suds/xsd/schema.py         |  411 ++++++++++++++
 suds/xsd/sxbase.py         |  661 +++++++++++++++++++++
 suds/xsd/sxbasic.py        |  857 ++++++++++++++++++++++++++++
 suds/xsd/sxbuiltin.py      |  255 +++++++++
 62 files changed, 14482 insertions(+), 27 deletions(-)
 create mode 100644 suds/__init__.py
 create mode 100644 suds/argparser.py
 create mode 100644 suds/bindings/__init__.py
 create mode 100644 suds/bindings/binding.py
 create mode 100644 suds/bindings/document.py
 create mode 100644 suds/bindings/multiref.py
 create mode 100644 suds/bindings/rpc.py
 create mode 100644 suds/builder.py
 create mode 100644 suds/cache.py
 create mode 100644 suds/client.py
 create mode 100644 suds/metrics.py
 create mode 100644 suds/mx/__init__.py
 create mode 100644 suds/mx/appender.py
 create mode 100644 suds/mx/basic.py
 create mode 100644 suds/mx/core.py
 create mode 100644 suds/mx/encoded.py
 create mode 100644 suds/mx/literal.py
 create mode 100644 suds/mx/typer.py
 create mode 100644 suds/options.py
 create mode 100644 suds/plugin.py
 create mode 100644 suds/properties.py
 create mode 100644 suds/reader.py
 create mode 100644 suds/resolver.py
 create mode 100644 suds/sax/__init__.py
 create mode 100644 suds/sax/attribute.py
 create mode 100644 suds/sax/date.py
 create mode 100644 suds/sax/document.py
 create mode 100644 suds/sax/element.py
 create mode 100644 suds/sax/enc.py
 create mode 100644 suds/sax/parser.py
 create mode 100644 suds/sax/text.py
 create mode 100644 suds/servicedefinition.py
 create mode 100644 suds/serviceproxy.py
 create mode 100644 suds/soaparray.py
 create mode 100644 suds/store.py
 create mode 100644 suds/sudsobject.py
 create mode 100644 suds/transport/__init__.py
 create mode 100644 suds/transport/http.py
 create mode 100644 suds/transport/https.py
 create mode 100644 suds/transport/options.py
 create mode 100644 suds/umx/__init__.py
 create mode 100644 suds/umx/attrlist.py
 create mode 100644 suds/umx/basic.py
 create mode 100644 suds/umx/core.py
 create mode 100644 suds/umx/encoded.py
 create mode 100644 suds/umx/typed.py
 create mode 100644 suds/version.py
 create mode 100644 suds/wsdl.py
 create mode 100644 suds/wsse.py
 create mode 100644 suds/xsd/__init__.py
 create mode 100644 suds/xsd/deplist.py
 create mode 100644 suds/xsd/doctor.py
 create mode 100644 suds/xsd/query.py
 create mode 100644 suds/xsd/schema.py
 create mode 100644 suds/xsd/sxbase.py
 create mode 100644 suds/xsd/sxbasic.py
 create mode 100644 suds/xsd/sxbuiltin.py

diff --git a/__init__.py b/__init__.py
index 6a92b00f..7ff5c6b2 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1 +1 @@
-all = [ "plugins" ]
+all = [ "plugins", 'suds' ]
diff --git a/biomodelsclient.py b/biomodelsclient.py
index 403da126..6a24ba28 100644
--- a/biomodelsclient.py
+++ b/biomodelsclient.py
@@ -45,7 +45,6 @@
 
 # Code:
 
-
 from suds.client import Client
 from suds.transport.http import HttpTransport as SudsHttpTransport
 import os
diff --git a/mgui.py b/mgui.py
index 9627137b..ede1ba95 100644
--- a/mgui.py
+++ b/mgui.py
@@ -1101,27 +1101,25 @@ class MWindow(QtGui.QMainWindow):
                               self.tr('Load model from file'))
 
         if dialog.exec_():
-            fileNames = dialog.selectedFiles()
-            for fileName in fileNames:
-                modelName = dialog.getTargetPath()
-                if '/' in modelName:
-                    raise mexception.ElementNameError('Model name cannot contain `/`')
-                ret = loadFile(str(fileName),'%s' %(modelName),merge=False)
-                #ret = loadFile(str(fileName), '/model/%s' % (modelName), merge=False)
-		        #Harsha: This will clear out object editor's objectpath and make it invisible
-                self.objectEditSlot('/',False)
-
-                # Harsha: if subtype is None, in case of cspace then pluginLookup = /cspace/None
-                #     which will not call kkit plugin so cleaning to /cspace
-                pluginLookup = '%s/%s' % (ret['modeltype'], ret['subtype'])
-                try:
-                    pluginName = subtype_plugin_map['%s/%s' % (ret['modeltype'], ret['subtype'])]
-                except KeyError:
-                    pluginName = 'default'
-                print 'Loaded model', ret['model'].path
-                # if not moose.exists(ret['model'].path+'/info'):
-                #     moose.Annotator(ret['model'].path+'/info')
-
+            valid = False
+            ret = []
+            ret,pluginName = self.checkPlugin(dialog)
+            if pluginName == 'kkit':
+                compt = moose.wildcardFind(ret['model'].path+'/##[ISA=ChemCompt]')
+                if not len(compt):
+                    reply = QtGui.QMessageBox.question(self, "Model is empty","Model has no compartment, atleast one compartment should exist to display the widget\n Do you want another file",
+                                               QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+                    if reply == QtGui.QMessageBox.Yes:
+                        dialog = LoaderDialog(self,self.tr('Load model from file'))
+                        if dialog.exec_():
+                            ret,pluginName = self.checkPlugin(dialog)
+                            ret,valid = self.dialog_check(ret)
+                    else:
+                        QtGui.QApplication.restoreOverrideCursor()        
+                        return
+                else:
+                    valid = True
+            if valid == True:
                 modelAnno = moose.Annotator(ret['model'].path+'/info')
                 if ret['subtype']:
                     modelAnno.modeltype = ret['subtype']
@@ -1133,6 +1131,46 @@ class MWindow(QtGui.QMainWindow):
                 if pluginName == 'kkit':
                     QtCore.QCoreApplication.sendEvent(self.plugin.getEditorView().getCentralWidget().view, QtGui.QKeyEvent(QtCore.QEvent.KeyPress, Qt.Qt.Key_A, Qt.Qt.NoModifier))
 
+    def checkPlugin(self,dialog):
+        fileNames = dialog.selectedFiles()
+        for fileName in fileNames:
+            modelName = dialog.getTargetPath()
+            if '/' in modelName:
+                raise mexception.ElementNameError('Model name cannot contain `/`')
+            ret = loadFile(str(fileName),'%s' %(modelName),merge=False)
+            #ret = loadFile(str(fileName), '/model/%s' % (modelName), merge=False)
+            #This will clear out object editor's objectpath and make it invisible
+            self.objectEditSlot('/',False)
+            #if subtype is None, in case of cspace then pluginLookup = /cspace/None
+            #     which will not call kkit plugin so cleaning to /cspace
+            pluginLookup = '%s/%s' % (ret['modeltype'], ret['subtype'])
+            try:
+                pluginName = subtype_plugin_map['%s/%s' % (ret['modeltype'], ret['subtype'])]
+            except KeyError:
+                pluginName = 'default'
+            print 'Loaded model', ret['model'].path
+            return ret,pluginName
+
+    def dialog_check(self,ret):
+        pluginLookup = '%s/%s' % (ret['modeltype'], ret['subtype'])
+        try:
+            pluginName = subtype_plugin_map['%s/%s' % (ret['modeltype'], ret['subtype'])]
+        except KeyError:
+            pluginName = 'default'
+        if pluginName == 'kkit':
+            compt = moose.wildcardFind(ret['model'].path+'/##[ISA=ChemCompt]')
+            if not len(compt):
+                reply = QtGui.QMessageBox.question(self, "Model is empty","Model has no compartment, atleast one compartment should exist to display the widget\n Do you want another file",
+                                           QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
+                if reply == QtGui.QMessageBox.Yes:
+                    dialog = LoaderDialog(self,self.tr('Load model from file'))
+                    if dialog.exec_():
+                        ret,pluginName = self.checkPlugin(dialog)
+                else:
+                    QtGui.QApplication.restoreOverrideCursor()        
+                    return
+            else:
+                return ret,True
     def newModelDialogSlot(self):
         #Harsha: Create a new dialog widget for model building
         self.popup.close()
diff --git a/mload.py b/mload.py
index fc643250..d495f2c8 100644
--- a/mload.py
+++ b/mload.py
@@ -56,13 +56,13 @@ from PyQt4 import QtGui, QtCore, Qt
 from plugins.setsolver import *
 
 def loadGenCsp(target,filename,solver="gsl"):
+    target = target.replace(" ", "")
     path = '/'+target
-    #Harsha: Moving the model under /modelname/model and graphs under /model/graphs.
+    #Moving the model under /modelname/model and graphs under /model/graphs.
     #This is passed while loading-time which will be easy for setting the stoich path
     mpath = '/'+target+'/'+"model"
     if moose.exists(mpath):
         moose.delete(mpath)
-    
     modelpath1 = moose.Neutral('%s' %(target))
     modelpath = moose.Neutral('%s/%s' %(modelpath1.path,"model"))
     model = moose.loadModel(filename, modelpath.path,solver)
diff --git a/plugins/kkitUtil.py b/plugins/kkitUtil.py
index 84f5a17c..7904407f 100644
--- a/plugins/kkitUtil.py
+++ b/plugins/kkitUtil.py
@@ -62,8 +62,11 @@ def colorCheck(fc_bgcolor,fcbg):
         elif fc_bgcolor.isdigit():
             """ color is int  a map from int to r,g,b triplets from pickled color map file """
             tc = int(fc_bgcolor)
-            tc = 2*tc
-            pickledColor = colorMap[tc]
+            tc = tc*2
+            if tc < len(colorMap):
+                pickledColor = colorMap[tc]
+            else:
+                pickledColor = (255, 0, 0)
             fc_bgcolor = QColor(*pickledColor)
 
         elif fc_bgcolor.isalpha() or fc_bgcolor.isalnum():
diff --git a/suds/__init__.py b/suds/__init__.py
new file mode 100644
index 00000000..1f8a6d90
--- /dev/null
+++ b/suds/__init__.py
@@ -0,0 +1,192 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Suds is a lightweight SOAP Python client providing a Web Service proxy.
+"""
+
+import sys
+
+
+#
+# Project properties
+#
+
+from version import __build__, __version__
+
+
+#
+# Exceptions
+#
+
+class MethodNotFound(Exception):
+    def __init__(self, name):
+        Exception.__init__(self, u"Method not found: '%s'" % name)
+
+class PortNotFound(Exception):
+    def __init__(self, name):
+        Exception.__init__(self, u"Port not found: '%s'" % name)
+
+class ServiceNotFound(Exception):
+    def __init__(self, name):
+        Exception.__init__(self, u"Service not found: '%s'" % name)
+
+class TypeNotFound(Exception):
+    def __init__(self, name):
+        Exception.__init__(self, u"Type not found: '%s'" % tostr(name))
+
+class BuildError(Exception):
+    msg = """
+        An error occurred while building an instance of (%s). As a result the
+        object you requested could not be constructed. It is recommended that
+        you construct the type manually using a Suds object. Please open a
+        ticket with a description of this error.
+        Reason: %s
+        """
+    def __init__(self, name, exception):
+        Exception.__init__(self, BuildError.msg % (name, exception))
+
+class SoapHeadersNotPermitted(Exception):
+    msg = """
+        Method (%s) was invoked with SOAP headers. The WSDL does not define
+        SOAP headers for this method. Retry without the soapheaders keyword
+        argument.
+        """
+    def __init__(self, name):
+        Exception.__init__(self, self.msg % name)
+
+class WebFault(Exception):
+    def __init__(self, fault, document):
+        if hasattr(fault, 'faultstring'):
+            Exception.__init__(self, u"Server raised fault: '%s'" %
+                fault.faultstring)
+        self.fault = fault
+        self.document = document
+
+
+#
+# Logging
+#
+
+class Repr:
+    def __init__(self, x):
+        self.x = x
+    def __str__(self):
+        return repr(self.x)
+
+
+#
+# Utility
+#
+
+class null:
+    """
+    The I{null} object.
+    Used to pass NULL for optional XML nodes.
+    """
+    pass
+
+def objid(obj):
+    return obj.__class__.__name__ + ':' + hex(id(obj))
+
+def tostr(object, encoding=None):
+    """ get a unicode safe string representation of an object """
+    if isinstance(object, basestring):
+        if encoding is None:
+            return object
+        else:
+            return object.encode(encoding)
+    if isinstance(object, tuple):
+        s = ['(']
+        for item in object:
+            if isinstance(item, basestring):
+                s.append(item)
+            else:
+                s.append(tostr(item))
+            s.append(', ')
+        s.append(')')
+        return ''.join(s)
+    if isinstance(object, list):
+        s = ['[']
+        for item in object:
+            if isinstance(item, basestring):
+                s.append(item)
+            else:
+                s.append(tostr(item))
+            s.append(', ')
+        s.append(']')
+        return ''.join(s)
+    if isinstance(object, dict):
+        s = ['{']
+        for item in object.items():
+            if isinstance(item[0], basestring):
+                s.append(item[0])
+            else:
+                s.append(tostr(item[0]))
+            s.append(' = ')
+            if isinstance(item[1], basestring):
+                s.append(item[1])
+            else:
+                s.append(tostr(item[1]))
+            s.append(', ')
+        s.append('}')
+        return ''.join(s)
+    try:
+        return unicode(object)
+    except:
+        return str(object)
+
+
+#
+# Python 3 compatibility
+#
+
+if sys.version_info < (3, 0):
+    from cStringIO import StringIO as BytesIO
+else:
+    from io import BytesIO
+
+# Idea from 'http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python'.
+class UnicodeMixin(object):
+    if sys.version_info >= (3, 0):
+        # For Python 3, __str__() and __unicode__() should be identical.
+        __str__ = lambda x: x.__unicode__()
+    else:
+        __str__ = lambda x: unicode(x).encode('utf-8')
+
+#   Used instead of byte literals because they are not supported on Python
+# versions prior to 2.6.
+def byte_str(s='', encoding='utf-8', input_encoding='utf-8', errors='strict'):
+    """
+    Returns a bytestring version of 's', encoded as specified in 'encoding'.
+
+    Accepts str & unicode objects, interpreting non-unicode strings as byte
+    strings encoded using the given input encoding.
+
+    """
+    assert isinstance(s, basestring)
+    if isinstance(s, unicode):
+        return s.encode(encoding, errors)
+    if s and encoding != input_encoding:
+        return s.decode(input_encoding, errors).encode(encoding, errors)
+    return s
+
+# Class used to represent a byte string. Useful for asserting that correct
+# string types are being passed around where needed.
+if sys.version_info >= (3, 0):
+    byte_str_class = bytes
+else:
+    byte_str_class = str
diff --git a/suds/argparser.py b/suds/argparser.py
new file mode 100644
index 00000000..84ac0262
--- /dev/null
+++ b/suds/argparser.py
@@ -0,0 +1,419 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić jurko.gospodnetic@pke.hr )
+
+"""
+Suds web service operation invocation function argument parser.
+
+See the parse_args() function description for more detailed information.
+
+"""
+
+__all__ = ["parse_args"]
+
+
+def parse_args(method_name, param_defs, args, kwargs, external_param_processor,
+    extra_parameter_errors):
+    """
+    Parse arguments for suds web service operation invocation functions.
+
+    Suds prepares Python function objects for invoking web service operations.
+    This function implements generic binding agnostic part of processing the
+    arguments passed when calling those function objects.
+
+    Argument parsing rules:
+      * Each input parameter element should be represented by single regular
+        Python function argument.
+      * At most one input parameter belonging to a single choice parameter
+        structure may have its value specified as something other than None.
+      * Positional arguments are mapped to choice group input parameters the
+        same as is done for a simple all/sequence group - each in turn.
+
+    Expects to be passed the web service operation's parameter definitions
+    (parameter name, type & optional ancestry information) in order and, based
+    on that, extracts the values for those parameter from the arguments
+    provided in the web service operation invocation call.
+
+    Ancestry information describes parameters constructed based on suds
+    library's automatic input parameter structure unwrapping. It is expected to
+    include the parameter's XSD schema 'ancestry' context, i.e. a list of all
+    the parent XSD schema tags containing the parameter's <element> tag. Such
+    ancestry context provides detailed information about how the parameter's
+    value is expected to be used, especially in relation to other input
+    parameters, e.g. at most one parameter value may be specified for
+    parameters directly belonging to the same choice input group.
+
+    Rules on acceptable ancestry items:
+      * Ancestry item's choice() method must return whether the item
+        represents a <choice> XSD schema tag.
+      * Passed ancestry items are used 'by address' internally and the same XSD
+        schema tag is expected to be identified by the exact same ancestry item
+        object during the whole argument processing.
+
+    During processing, each parameter's definition and value, together with any
+    additional pertinent information collected from the encountered parameter
+    definition structure, is passed on to the provided external parameter
+    processor function. There that information is expected to be used to
+    construct the actual binding specific web service operation invocation
+    request.
+
+    Raises a TypeError exception in case any argument related errors are
+    detected. The exceptions raised have been constructed to make them as
+    similar as possible to their respective exceptions raised during regular
+    Python function argument checking.
+
+    Does not support multiple same-named input parameters.
+
+    """
+    arg_parser = _ArgParser(method_name, param_defs, external_param_processor)
+    return arg_parser(args, kwargs, extra_parameter_errors)
+
+
+class _ArgParser:
+    """Internal argument parser implementation function object."""
+
+    def __init__(self, method_name, param_defs, external_param_processor):
+        self.__method_name = method_name
+        self.__param_defs = param_defs
+        self.__external_param_processor = external_param_processor
+        self.__stack = []
+
+    def __call__(self, args, kwargs, extra_parameter_errors):
+        """
+        Runs the main argument parsing operation.
+
+        Passed args & kwargs objects are not modified during parsing.
+
+        Returns an informative 2-tuple containing the number of required &
+        allowed arguments.
+
+        """
+        assert not self.active(), "recursive argument parsing not allowed"
+        self.__init_run(args, kwargs, extra_parameter_errors)
+        try:
+            self.__process_parameters()
+            return self.__all_parameters_processed()
+        finally:
+            self.__cleanup_run()
+            assert not self.active()
+
+    def active(self):
+        """
+        Return whether this object is currently running argument processing.
+
+        Used to avoid recursively entering argument processing from within an
+        external parameter processor.
+
+        """
+        return bool(self.__stack)
+
+    def __all_parameters_processed(self):
+        """
+        Finish the argument processing.
+
+        Should be called after all the web service operation's parameters have
+        been successfully processed and, afterwards, no further parameter
+        processing is allowed.
+
+        Returns a 2-tuple containing the number of required & allowed
+        arguments.
+
+        See the _ArgParser class description for more detailed information.
+
+        """
+        assert self.active()
+        sentinel_frame = self.__stack[0]
+        self.__pop_frames_above(sentinel_frame)
+        assert len(self.__stack) == 1
+        self.__pop_top_frame()
+        assert not self.active()
+        args_required = sentinel_frame.args_required()
+        args_allowed = sentinel_frame.args_allowed()
+        self.__check_for_extra_arguments(args_required, args_allowed)
+        return args_required, args_allowed
+
+    def __check_for_extra_arguments(self, args_required, args_allowed):
+        """
+        Report an error in case any extra arguments are detected.
+
+        Does nothing if reporting extra arguments as exceptions has not been
+        enabled.
+
+        May only be called after the argument processing has been completed.
+
+        """
+        assert not self.active()
+        if not self.__extra_parameter_errors:
+            return
+
+        if self.__kwargs:
+            param_name = self.__kwargs.keys()[0]
+            if param_name in self.__params_with_arguments:
+                msg = "got multiple values for parameter '%s'"
+            else:
+                msg = "got an unexpected keyword argument '%s'"
+            self.__error(msg % (param_name,))
+
+        if self.__args:
+            def plural_suffix(count):
+                if count == 1:
+                    return ""
+                return "s"
+            def plural_was_were(count):
+                if count == 1:
+                    return "was"
+                return "were"
+            expected = args_required
+            if args_required != args_allowed:
+                expected = "%d to %d" % (args_required, args_allowed)
+            given = self.__args_count
+            msg_parts = ["takes %s positional argument" % (expected,),
+                plural_suffix(expected), " but %d " % (given,),
+                plural_was_were(given), " given"]
+            self.__error("".join(msg_parts))
+
+    def __cleanup_run(self):
+        """Cleans up after a completed argument parsing run."""
+        self.__stack = []
+        assert not self.active()
+
+    def __error(self, message):
+        """Report an argument processing error."""
+        raise TypeError("%s() %s" % (self.__method_name, message))
+
+    def __frame_factory(self, ancestry_item):
+        """Construct a new frame representing the given ancestry item."""
+        frame_class = Frame
+        if ancestry_item is not None and ancestry_item.choice():
+            frame_class = ChoiceFrame
+        return frame_class(ancestry_item, self.__error,
+            self.__extra_parameter_errors)
+
+    def __get_param_value(self, name):
+        """
+        Extract a parameter value from the remaining given arguments.
+
+        Returns a 2-tuple consisting of the following:
+          * Boolean indicating whether an argument has been specified for the
+            requested input parameter.
+          * Parameter value.
+
+        """
+        if self.__args:
+            return True, self.__args.pop(0)
+        try:
+            value = self.__kwargs.pop(name)
+        except KeyError:
+            return False, None
+        return True, value
+
+    def __in_choice_context(self):
+        """
+        Whether we are currently processing a choice parameter group.
+
+        This includes processing a parameter defined directly or indirectly
+        within such a group.
+
+        May only be called during parameter processing or the result will be
+        calculated based on the context left behind by the previous parameter
+        processing if any.
+
+        """
+        for x in self.__stack:
+            if x.__class__ is ChoiceFrame:
+                return True
+        return False
+
+    def __init_run(self, args, kwargs, extra_parameter_errors):
+        """Initializes data for a new argument parsing run."""
+        assert not self.active()
+        self.__args = list(args)
+        self.__kwargs = dict(kwargs)
+        self.__extra_parameter_errors = extra_parameter_errors
+        self.__args_count = len(args) + len(kwargs)
+        self.__params_with_arguments = set()
+        self.__stack = []
+        self.__push_frame(None)
+
+    def __match_ancestry(self, ancestry):
+        """
+        Find frames matching the given ancestry.
+
+        Returns a tuple containing the following:
+          * Topmost frame matching the given ancestry or the bottom-most sentry
+            frame if no frame matches.
+          * Unmatched ancestry part.
+
+        """
+        stack = self.__stack
+        if len(stack) == 1:
+            return stack[0], ancestry
+        previous = stack[0]
+        for frame, n in zip(stack[1:], xrange(len(ancestry))):
+            if frame.id() is not ancestry[n]:
+                return previous, ancestry[n:]
+            previous = frame
+        return frame, ancestry[n + 1:]
+
+    def __pop_frames_above(self, frame):
+        """Pops all the frames above, but not including the given frame."""
+        while self.__stack[-1] is not frame:
+            self.__pop_top_frame()
+        assert self.__stack
+
+    def __pop_top_frame(self):
+        """Pops the top frame off the frame stack."""
+        popped = self.__stack.pop()
+        if self.__stack:
+            self.__stack[-1].process_subframe(popped)
+
+    def __process_parameter(self, param_name, param_type, ancestry=None):
+        """Collect values for a given web service operation input parameter."""
+        assert self.active()
+        param_optional = param_type.optional()
+        has_argument, value = self.__get_param_value(param_name)
+        if has_argument:
+            self.__params_with_arguments.add(param_name)
+        self.__update_context(ancestry)
+        self.__stack[-1].process_parameter(param_optional, value is not None)
+        self.__external_param_processor(param_name, param_type,
+            self.__in_choice_context(), value)
+
+    def __process_parameters(self):
+        """Collect values for given web service operation input parameters."""
+        for pdef in self.__param_defs:
+            self.__process_parameter(*pdef)
+
+    def __push_frame(self, ancestry_item):
+        """Push a new frame on top of the frame stack."""
+        frame = self.__frame_factory(ancestry_item)
+        self.__stack.append(frame)
+
+    def __push_frames(self, ancestry):
+        """
+        Push new frames representing given ancestry items.
+
+        May only be given ancestry items other than None. Ancestry item None
+        represents the internal sentinel item and should never appear in a
+        given parameter's ancestry information.
+
+        """
+        for x in ancestry:
+            assert x is not None
+            self.__push_frame(x)
+
+    def __update_context(self, ancestry):
+        if not ancestry:
+            return
+        match_result = self.__match_ancestry(ancestry)
+        last_matching_frame, unmatched_ancestry = match_result
+        self.__pop_frames_above(last_matching_frame)
+        self.__push_frames(unmatched_ancestry)
+
+
+class Frame:
+    """
+    Base _ArgParser context frame.
+
+    When used directly, as opposed to using a derived class, may represent any
+    input parameter context/ancestry item except a choice order indicator.
+
+    """
+
+    def __init__(self, id, error, extra_parameter_errors):
+        """
+        Construct a new Frame instance.
+
+        Passed error function is used to report any argument checking errors.
+
+        """
+        assert self.__class__ != Frame or not id or not id.choice()
+        self.__id = id
+        self._error = error
+        self._extra_parameter_errors = extra_parameter_errors
+        self._args_allowed = 0
+        self._args_required = 0
+        self._has_value = False
+
+    def args_allowed(self):
+        return self._args_allowed
+
+    def args_required(self):
+        return self._args_required
+
+    def has_value(self):
+        return self._has_value
+
+    def id(self):
+        return self.__id
+
+    def process_parameter(self, optional, has_value):
+        args_required = 1
+        if optional:
+            args_required = 0
+        self._process_item(has_value, 1, args_required)
+
+    def process_subframe(self, subframe):
+        self._process_item(
+            subframe.has_value(),
+            subframe.args_allowed(),
+            subframe.args_required())
+
+    def _process_item(self, has_value, args_allowed, args_required):
+        self._args_allowed += args_allowed
+        self._args_required += args_required
+        if has_value:
+            self._has_value = True
+
+
+class ChoiceFrame(Frame):
+    """
+    _ArgParser context frame representing a choice order indicator.
+
+    A choice requires as many input arguments as are needed to satisfy the
+    least requiring of its items. For example, if we use I(n) to identify an
+    item requiring n parameter, then a choice containing I(2), I(3) & I(7)
+    requires 2 arguments while a choice containing I(5) & I(4) requires 4.
+
+    Accepts an argument for each of its contained elements but allows at most
+    one of its directly contained items to have a defined value.
+
+    """
+
+    def __init__(self, id, error, extra_parameter_errors):
+        assert id.choice()
+        Frame.__init__(self, id, error, extra_parameter_errors)
+        self.__has_item = False
+
+    def _process_item(self, has_value, args_allowed, args_required):
+        self._args_allowed += args_allowed
+        self.__update_args_required_for_item(args_required)
+        self.__update_has_value_for_item(has_value)
+
+    def __update_args_required_for_item(self, item_args_required):
+        if not self.__has_item:
+            self.__has_item = True
+            self._args_required = item_args_required
+            return
+        self._args_required = min(self.args_required(), item_args_required)
+
+    def __update_has_value_for_item(self, item_has_value):
+        if item_has_value:
+            if self.has_value() and self._extra_parameter_errors:
+                self._error("got multiple values for a single choice "
+                    "parameter")
+            self._has_value = True
diff --git a/suds/bindings/__init__.py b/suds/bindings/__init__.py
new file mode 100644
index 00000000..1704fa9d
--- /dev/null
+++ b/suds/bindings/__init__.py
@@ -0,0 +1,18 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides modules containing classes to support Web Services (SOAP) bindings.
+"""
diff --git a/suds/bindings/binding.py b/suds/bindings/binding.py
new file mode 100644
index 00000000..62f641c2
--- /dev/null
+++ b/suds/bindings/binding.py
@@ -0,0 +1,474 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides classes for (WS) SOAP bindings.
+"""
+
+from suds import *
+from suds.sax import Namespace
+from suds.sax.document import Document
+from suds.sax.element import Element
+from suds.sudsobject import Factory
+from suds.mx import Content
+from suds.mx.literal import Literal as MxLiteral
+from suds.umx.typed import Typed as UmxTyped
+from suds.bindings.multiref import MultiRef
+from suds.xsd.query import TypeQuery, ElementQuery
+from suds.xsd.sxbasic import Element as SchemaElement
+from suds.options import Options
+from suds.plugin import PluginContainer
+
+from copy import deepcopy
+
+
+envns = ('SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/')
+
+
+class Binding:
+    """
+    The SOAP binding class used to process outgoing and incoming SOAP messages
+    per the WSDL port binding.
+    @ivar wsdl: The WSDL.
+    @type wsdl: L{suds.wsdl.Definitions}
+    @ivar schema: The collective schema contained within the WSDL.
+    @type schema: L{xsd.schema.Schema}
+    @ivar options: A dictionary options.
+    @type options: L{Options}
+    """
+
+    def __init__(self, wsdl):
+        """
+        @param wsdl: A WSDL.
+        @type wsdl: L{wsdl.Definitions}
+        """
+        self.wsdl = wsdl
+        self.multiref = MultiRef()
+
+    def schema(self):
+        return self.wsdl.schema
+
+    def options(self):
+        return self.wsdl.options
+
+    def unmarshaller(self):
+        """
+        Get the appropriate schema based XML decoder.
+        @return: Typed unmarshaller.
+        @rtype: L{UmxTyped}
+        """
+        return UmxTyped(self.schema())
+
+    def marshaller(self):
+        """
+        Get the appropriate XML encoder.
+        @return: An L{MxLiteral} marshaller.
+        @rtype: L{MxLiteral}
+        """
+        return MxLiteral(self.schema(), self.options().xstq)
+
+    def param_defs(self, method):
+        """
+        Get parameter definitions.
+        Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject})
+        @param method: A service method.
+        @type method: I{service.Method}
+        @return: A collection of parameter definitions
+        @rtype: [I{pdef},..]
+        """
+        raise Exception, 'not implemented'
+
+    def get_message(self, method, args, kwargs):
+        """
+        Get a SOAP message for the specified method, args and SOAP headers.
+        This is the entry point for creating an outbound SOAP message.
+        @param method: The method being invoked.
+        @type method: I{service.Method}
+        @param args: A list of args for the method invoked.
+        @type args: list
+        @param kwargs: Named (keyword) args for the method invoked.
+        @type kwargs: dict
+        @return: The SOAP envelope.
+        @rtype: L{Document}
+        """
+
+        content = self.headercontent(method)
+        header = self.header(content)
+        content = self.bodycontent(method, args, kwargs)
+        body = self.body(content)
+        env = self.envelope(header, body)
+        if self.options().prefixes:
+            body.normalizePrefixes()
+            env.promotePrefixes()
+        else:
+            env.refitPrefixes()
+        return Document(env)
+
+    def get_reply(self, method, replyroot):
+        """
+        Process the I{reply} for the specified I{method} by unmarshalling it
+        into into Python object(s).
+        @param method: The name of the invoked method.
+        @type method: str
+        @param replyroot: The reply XML root node received after invoking the
+            specified method.
+        @type reply: L{Element}
+        @return: The unmarshalled reply.  The returned value is an L{Object} or
+            a I{list} depending on whether the service returns a single object
+            or a collection.
+        @rtype: L{Object} or I{list}
+        """
+        soapenv = replyroot.getChild('Envelope', envns)
+        soapenv.promotePrefixes()
+        soapbody = soapenv.getChild('Body', envns)
+        soapbody = self.multiref.process(soapbody)
+        nodes = self.replycontent(method, soapbody)
+        rtypes = self.returned_types(method)
+        if len(rtypes) > 1:
+            return self.replycomposite(rtypes, nodes)
+        if len(rtypes) == 0:
+            return
+        if rtypes[0].multi_occurrence():
+            return self.replylist(rtypes[0], nodes)
+        if len(nodes):
+            resolved = rtypes[0].resolve(nobuiltin=True)
+            return self.unmarshaller().process(nodes[0], resolved)
+
+    def replylist(self, rt, nodes):
+        """
+        Construct a I{list} reply. This mehod is called when it has been
+        detected that the reply is a list.
+        @param rt: The return I{type}.
+        @type rt: L{suds.xsd.sxbase.SchemaObject}
+        @param nodes: A collection of XML nodes.
+        @type nodes: [L{Element},...]
+        @return: A list of I{unmarshalled} objects.
+        @rtype: [L{Object},...]
+        """
+        result = []
+        resolved = rt.resolve(nobuiltin=True)
+        unmarshaller = self.unmarshaller()
+        for node in nodes:
+            sobject = unmarshaller.process(node, resolved)
+            result.append(sobject)
+        return result
+
+    def replycomposite(self, rtypes, nodes):
+        """
+        Construct a I{composite} reply. This method is called when it has been
+        detected that the reply has multiple root nodes.
+        @param rtypes: A list of known return I{types}.
+        @type rtypes: [L{suds.xsd.sxbase.SchemaObject},...]
+        @param nodes: A collection of XML nodes.
+        @type nodes: [L{Element},...]
+        @return: The I{unmarshalled} composite object.
+        @rtype: L{Object},...
+        """
+        dictionary = {}
+        for rt in rtypes:
+            dictionary[rt.name] = rt
+        unmarshaller = self.unmarshaller()
+        composite = Factory.object('reply')
+        for node in nodes:
+            tag = node.name
+            rt = dictionary.get(tag, None)
+            if rt is None:
+                if node.get('id') is None:
+                    raise Exception('<%s/> not mapped to message part' % tag)
+                else:
+                    continue
+            resolved = rt.resolve(nobuiltin=True)
+            sobject = unmarshaller.process(node, resolved)
+            value = getattr(composite, tag, None)
+            if value is None:
+                if rt.multi_occurrence():
+                    value = []
+                    setattr(composite, tag, value)
+                    value.append(sobject)
+                else:
+                    setattr(composite, tag, sobject)
+            else:
+                if not isinstance(value, list):
+                    value = [value,]
+                    setattr(composite, tag, value)
+                value.append(sobject)
+        return composite
+
+    def mkparam(self, method, pdef, object):
+        """
+        Builds a parameter for the specified I{method} using the parameter
+        definition (pdef) and the specified value (object).
+        @param method: A method name.
+        @type method: str
+        @param pdef: A parameter definition.
+        @type pdef: tuple: (I{name}, L{xsd.sxbase.SchemaObject})
+        @param object: The parameter value.
+        @type object: any
+        @return: The parameter fragment.
+        @rtype: L{Element}
+        """
+        marshaller = self.marshaller()
+        content = Content(tag=pdef[0], value=object, type=pdef[1],
+            real=pdef[1].resolve())
+        return marshaller.process(content)
+
+    def mkheader(self, method, hdef, object):
+        """
+        Builds a soapheader for the specified I{method} using the header
+        definition (hdef) and the specified value (object).
+        @param method: A method name.
+        @type method: str
+        @param hdef: A header definition.
+        @type hdef: tuple: (I{name}, L{xsd.sxbase.SchemaObject})
+        @param object: The header value.
+        @type object: any
+        @return: The parameter fragment.
+        @rtype: L{Element}
+        """
+        marshaller = self.marshaller()
+        if isinstance(object, (list, tuple)):
+            tags = []
+            for item in object:
+                tags.append(self.mkheader(method, hdef, item))
+            return tags
+        content = Content(tag=hdef[0], value=object, type=hdef[1])
+        return marshaller.process(content)
+
+    def envelope(self, header, body):
+        """
+        Build the B{<Envelope/>} for a SOAP outbound message.
+        @param header: The SOAP message B{header}.
+        @type header: L{Element}
+        @param body: The SOAP message B{body}.
+        @type body: L{Element}
+        @return: The SOAP envelope containing the body and header.
+        @rtype: L{Element}
+        """
+        env = Element('Envelope', ns=envns)
+        env.addPrefix(Namespace.xsins[0], Namespace.xsins[1])
+        env.append(header)
+        env.append(body)
+        return env
+
+    def header(self, content):
+        """
+        Build the B{<Body/>} for a SOAP outbound message.
+        @param content: The header content.
+        @type content: L{Element}
+        @return: the SOAP body fragment.
+        @rtype: L{Element}
+        """
+        header = Element('Header', ns=envns)
+        header.append(content)
+        return header
+
+    def bodycontent(self, method, args, kwargs):
+        """
+        Get the content for the SOAP I{body} node.
+        @param method: A service method.
+        @type method: I{service.Method}
+        @param args: method parameter values
+        @type args: list
+        @param kwargs: Named (keyword) args for the method invoked.
+        @type kwargs: dict
+        @return: The XML content for the <body/>
+        @rtype: [L{Element},..]
+        """
+        raise Exception, 'not implemented'
+
+    def headercontent(self, method):
+        """
+        Get the content for the SOAP I{Header} node.
+        @param method: A service method.
+        @type method: I{service.Method}
+        @return: The XML content for the <body/>
+        @rtype: [L{Element},..]
+        """
+        n = 0
+        content = []
+        wsse = self.options().wsse
+        if wsse is not None:
+            content.append(wsse.xml())
+        headers = self.options().soapheaders
+        if not isinstance(headers, (tuple,list,dict)):
+            headers = (headers,)
+        if len(headers) == 0:
+            return content
+        pts = self.headpart_types(method)
+        if isinstance(headers, (tuple,list)):
+            for header in headers:
+                if isinstance(header, Element):
+                    content.append(deepcopy(header))
+                    continue
+                if len(pts) == n: break
+                h = self.mkheader(method, pts[n], header)
+                ns = pts[n][1].namespace('ns0')
+                h.setPrefix(ns[0], ns[1])
+                content.append(h)
+                n += 1
+        else:
+            for pt in pts:
+                header = headers.get(pt[0])
+                if header is None:
+                    continue
+                h = self.mkheader(method, pt, header)
+                ns = pt[1].namespace('ns0')
+                h.setPrefix(ns[0], ns[1])
+                content.append(h)
+        return content
+
+    def replycontent(self, method, body):
+        """
+        Get the reply body content.
+        @param method: A service method.
+        @type method: I{service.Method}
+        @param body: The SOAP body.
+        @type body: L{Element}
+        @return: The body content.
+        @rtype: [L{Element},...]
+        """
+        raise Exception, 'not implemented'
+
+    def body(self, content):
+        """
+        Build the B{<Body/>} for a SOAP outbound message.
+        @param content: The body content.
+        @type content: L{Element}
+        @return: The SOAP body fragment.
+        @rtype: L{Element}
+        """
+        body = Element('Body', ns=envns)
+        body.append(content)
+        return body
+
+    def bodypart_types(self, method, input=True):
+        """
+        Get a list of I{parameter definitions} (pdef) defined for the specified
+        method. Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject}).
+        @param method: A service method.
+        @type method: I{service.Method}
+        @param input: Defines input/output message.
+        @type input: boolean
+        @return:  A list of parameter definitions
+        @rtype: [I{pdef},]
+        """
+        result = []
+        if input:
+            parts = method.soap.input.body.parts
+        else:
+            parts = method.soap.output.body.parts
+        for p in parts:
+            if p.element is not None:
+                query = ElementQuery(p.element)
+            else:
+                query = TypeQuery(p.type)
+            pt = query.execute(self.schema())
+            if pt is None:
+                raise TypeNotFound(query.ref)
+            if p.type is not None:
+                pt = PartElement(p.name, pt)
+            if input:
+                if pt.name is None:
+                    result.append((p.name, pt))
+                else:
+                    result.append((pt.name, pt))
+            else:
+                result.append(pt)
+        return result
+
+    def headpart_types(self, method, input=True):
+        """
+        Get a list of I{parameter definitions} (pdef) defined for the specified
+        method. Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject}).
+        @param method: A service method.
+        @type method: I{service.Method}
+        @param input: Defines input/output message.
+        @type input: boolean
+        @return:  A list of parameter definitions
+        @rtype: [I{pdef},]
+        """
+        result = []
+        if input:
+            headers = method.soap.input.headers
+        else:
+            headers = method.soap.output.headers
+        for header in headers:
+            part = header.part
+            if part.element is not None:
+                query = ElementQuery(part.element)
+            else:
+                query = TypeQuery(part.type)
+            pt = query.execute(self.schema())
+            if pt is None:
+                raise TypeNotFound(query.ref)
+            if part.type is not None:
+                pt = PartElement(part.name, pt)
+            if input:
+                if pt.name is None:
+                    result.append((part.name, pt))
+                else:
+                    result.append((pt.name, pt))
+            else:
+                result.append(pt)
+        return result
+
+    def returned_types(self, method):
+        """
+        Get the L{xsd.sxbase.SchemaObject} returned by the I{method}.
+        @param method: A service method.
+        @type method: I{service.Method}
+        @return: The name of the type return by the method.
+        @rtype: [I{rtype},..]
+        """
+        result = []
+        for rt in self.bodypart_types(method, input=False):
+            result.append(rt)
+        return result
+
+
+class PartElement(SchemaElement):
+    """
+    A part used to represent a message part when the part
+    references a schema type and thus assumes to be an element.
+    @ivar resolved: The part type.
+    @type resolved: L{suds.xsd.sxbase.SchemaObject}
+    """
+
+    def __init__(self, name, resolved):
+        """
+        @param name: The part name.
+        @type name: str
+        @param resolved: The part type.
+        @type resolved: L{suds.xsd.sxbase.SchemaObject}
+        """
+        root = Element('element', ns=Namespace.xsdns)
+        SchemaElement.__init__(self, resolved.schema, root)
+        self.__resolved = resolved
+        self.name = name
+        self.form_qualified = False
+
+    def implany(self):
+        return self
+
+    def optional(self):
+        return True
+
+    def namespace(self, prefix=None):
+        return Namespace.default
+
+    def resolve(self, nobuiltin=False):
+        if nobuiltin and self.__resolved.builtin():
+            return self
+        return self.__resolved
diff --git a/suds/bindings/document.py b/suds/bindings/document.py
new file mode 100644
index 00000000..84ef566e
--- /dev/null
+++ b/suds/bindings/document.py
@@ -0,0 +1,157 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides classes for the (WS) SOAP I{document/literal} binding.
+
+"""
+
+from suds import *
+from suds.argparser import parse_args
+from suds.bindings.binding import Binding
+from suds.sax.element import Element
+
+
+class Document(Binding):
+    """
+    The document/literal style. Literal is the only (@use) supported since
+    document/encoded is pretty much dead.
+
+    Although the SOAP specification supports multiple documents within the SOAP
+    <body/>, it is very uncommon. As such, suds library supports presenting an
+    I{RPC} view of service methods defined with only a single document
+    parameter. To support the complete specification, service methods defined
+    with multiple documents (multiple message parts), are still presented using
+    a full I{document} view.
+
+    More detailed description:
+
+    An interface is considered I{wrapped} if:
+      - There is exactly one message part in that interface.
+      - The message part resolves to an element of a non-builtin type.
+    Otherwise it is considered I{bare}.
+
+    I{Bare} interface is interpreted directly as specified in the WSDL schema,
+    with each message part represented by a single parameter in the suds
+    library web service operation proxy interface (input or output).
+
+    I{Wrapped} interface is interpreted without the external wrapping document
+    structure, with each of its contained elements passed through suds
+    library's web service operation proxy interface (input or output)
+    individually instead of as a single I{document} object.
+
+    """
+    def bodycontent(self, method, args, kwargs):
+        if not len(method.soap.input.body.parts):
+            return ()
+        wrapped = method.soap.input.body.wrapped
+        if wrapped:
+            pts = self.bodypart_types(method)
+            root = self.document(pts[0])
+        else:
+            root = []
+
+        def add_param(param_name, param_type, in_choice_context, value):
+            """
+            Construct request data for the given input parameter.
+
+            Called by our argument parser for every input parameter, in order.
+
+            """
+            # Do not construct request data for undefined input parameters
+            # defined inside a choice order indicator. An empty choice
+            # parameter can still be included in the constructed request by
+            # explicitly providing an empty string value for it.
+            #TODO: This functionality might be better placed inside the
+            # mkparam() function but to do that we would first need to better
+            # understand how different Binding subclasses in suds work and how
+            # they would be affected by this change.
+            if in_choice_context and value is None:
+                return
+
+            # Construct request data for the current input parameter.
+            pdef = (param_name, param_type)
+            p = self.mkparam(method, pdef, value)
+            if p is None:
+                return
+            if not wrapped:
+                ns = param_type.namespace("ns0")
+                p.setPrefix(ns[0], ns[1])
+            root.append(p)
+
+        parse_args(method.name, self.param_defs(method), args, kwargs,
+            add_param, self.options().extraArgumentErrors)
+
+        return root
+
+    def replycontent(self, method, body):
+        wrapped = method.soap.output.body.wrapped
+        if wrapped:
+            return body[0].children
+        return body.children
+
+    def document(self, wrapper):
+        """
+        Get the document root. For I{document/literal}, this is the name of the
+        wrapper element qualified by the schema's target namespace.
+        @param wrapper: The method name.
+        @type wrapper: L{xsd.sxbase.SchemaObject}
+        @return: A root element.
+        @rtype: L{Element}
+        """
+        tag = wrapper[1].name
+        ns = wrapper[1].namespace("ns0")
+        return Element(tag, ns=ns)
+
+    def mkparam(self, method, pdef, object):
+        """
+        Expand list parameters into individual parameters each with the type
+        information. This is because in document arrays are simply
+        multi-occurrence elements.
+
+        """
+        if isinstance(object, (list, tuple)):
+            tags = []
+            for item in object:
+                tags.append(self.mkparam(method, pdef, item))
+            return tags
+        return Binding.mkparam(self, method, pdef, object)
+
+    def param_defs(self, method):
+        """Get parameter definitions for document literal."""
+        pts = self.bodypart_types(method)
+        wrapped = method.soap.input.body.wrapped
+        if not wrapped:
+            return pts
+        result = []
+        for p in pts:
+            for child, ancestry in p[1].resolve():
+                if not child.isattr():
+                    result.append((child.name, child, ancestry))
+        return result
+
+    def returned_types(self, method):
+        result = []
+        wrapped = method.soap.output.body.wrapped
+        rts = self.bodypart_types(method, input=False)
+        if wrapped:
+            for pt in rts:
+                resolved = pt.resolve(nobuiltin=True)
+                for child, ancestry in resolved:
+                    result.append(child)
+                break
+        else:
+            result += rts
+        return result
diff --git a/suds/bindings/multiref.py b/suds/bindings/multiref.py
new file mode 100644
index 00000000..52fa47ad
--- /dev/null
+++ b/suds/bindings/multiref.py
@@ -0,0 +1,124 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides classes for handling soap multirefs.
+"""
+
+from suds import *
+from suds.sax.element import Element
+
+
+soapenc = (None, 'http://schemas.xmlsoap.org/soap/encoding/')
+
+class MultiRef:
+    """
+    Resolves and replaces multirefs.
+    @ivar nodes: A list of non-multiref nodes.
+    @type nodes: list
+    @ivar catalog: A dictionary of multiref nodes by id.
+    @type catalog: dict
+    """
+
+    def __init__(self):
+        self.nodes = []
+        self.catalog = {}
+
+    def process(self, body):
+        """
+        Process the specified soap envelope body and replace I{multiref} node
+        references with the contents of the referenced node.
+        @param body: A soap envelope body node.
+        @type body: L{Element}
+        @return: The processed I{body}
+        @rtype: L{Element}
+        """
+        self.nodes = []
+        self.catalog = {}
+        self.build_catalog(body)
+        self.update(body)
+        body.children = self.nodes
+        return body
+
+    def update(self, node):
+        """
+        Update the specified I{node} by replacing the I{multiref} references with
+        the contents of the referenced nodes and remove the I{href} attribute.
+        @param node: A node to update.
+        @type node: L{Element}
+        @return: The updated node
+        @rtype: L{Element}
+        """
+        self.replace_references(node)
+        for c in node.children:
+            self.update(c)
+        return node
+
+    def replace_references(self, node):
+        """
+        Replacing the I{multiref} references with the contents of the
+        referenced nodes and remove the I{href} attribute.  Warning:  since
+        the I{ref} is not cloned,
+        @param node: A node to update.
+        @type node: L{Element}
+        """
+        href = node.getAttribute('href')
+        if href is None:
+            return
+        id = href.getValue()
+        ref = self.catalog.get(id)
+        if ref is None:
+            import logging
+            log = logging.getLogger(__name__)
+            log.error('soap multiref: %s, not-resolved', id)
+            return
+        node.append(ref.children)
+        node.setText(ref.getText())
+        for a in ref.attributes:
+            if a.name != 'id':
+                node.append(a)
+        node.remove(href)
+
+    def build_catalog(self, body):
+        """
+        Create the I{catalog} of multiref nodes by id and the list of
+        non-multiref nodes.
+        @param body: A soap envelope body node.
+        @type body: L{Element}
+        """
+        for child in body.children:
+            if self.soaproot(child):
+                self.nodes.append(child)
+            id = child.get('id')
+            if id is None: continue
+            key = '#%s' % id
+            self.catalog[key] = child
+
+    def soaproot(self, node):
+        """
+        Get whether the specified I{node} is a soap encoded root.
+        This is determined by examining @soapenc:root='1'.
+        The node is considered to be a root when the attribute
+        is not specified.
+        @param node: A node to evaluate.
+        @type node: L{Element}
+        @return: True if a soap encoded root.
+        @rtype: bool
+        """
+        root = node.getAttribute('root', ns=soapenc)
+        if root is None:
+            return True
+        else:
+            return ( root.value == '1' )
diff --git a/suds/bindings/rpc.py b/suds/bindings/rpc.py
new file mode 100644
index 00000000..d164960d
--- /dev/null
+++ b/suds/bindings/rpc.py
@@ -0,0 +1,91 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides classes for the (WS) SOAP I{rpc/literal} and I{rpc/encoded} bindings.
+"""
+
+from suds import *
+from suds.mx.encoded import Encoded as MxEncoded
+from suds.umx.encoded import Encoded as UmxEncoded
+from suds.bindings.binding import Binding, envns
+from suds.sax.element import Element
+
+
+encns = ('SOAP-ENC', 'http://schemas.xmlsoap.org/soap/encoding/')
+
+class RPC(Binding):
+    """
+    RPC/Literal binding style.
+    """
+
+    def param_defs(self, method):
+        return self.bodypart_types(method)
+
+    def envelope(self, header, body):
+        env = Binding.envelope(self, header, body)
+        env.addPrefix(encns[0], encns[1])
+        env.set('%s:encodingStyle' % envns[0],
+                'http://schemas.xmlsoap.org/soap/encoding/')
+        return env
+
+    def bodycontent(self, method, args, kwargs):
+        n = 0
+        root = self.method(method)
+        for pd in self.param_defs(method):
+            if n < len(args):
+                value = args[n]
+            else:
+                value = kwargs.get(pd[0])
+            p = self.mkparam(method, pd, value)
+            if p is not None:
+                root.append(p)
+            n += 1
+        return root
+
+    def replycontent(self, method, body):
+        return body[0].children
+
+    def method(self, method):
+        """
+        Get the document root.  For I{rpc/(literal|encoded)}, this is the
+        name of the method qualified by the schema tns.
+        @param method: A service method.
+        @type method: I{service.Method}
+        @return: A root element.
+        @rtype: L{Element}
+        """
+        ns = method.soap.input.body.namespace
+        if ns[0] is None:
+            ns = ('ns0', ns[1])
+        method = Element(method.name, ns=ns)
+        return method
+
+
+class Encoded(RPC):
+    """
+    RPC/Encoded (section 5)  binding style.
+    """
+
+    def marshaller(self):
+        return MxEncoded(self.schema())
+
+    def unmarshaller(self):
+        """
+        Get the appropriate schema based XML decoder.
+        @return: Typed unmarshaller.
+        @rtype: L{UmxTyped}
+        """
+        return UmxEncoded(self.schema())
diff --git a/suds/builder.py b/suds/builder.py
new file mode 100644
index 00000000..8ebfb385
--- /dev/null
+++ b/suds/builder.py
@@ -0,0 +1,117 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{builder} module provides an wsdl/xsd defined types factory
+"""
+
+from suds import *
+from suds.sudsobject import Factory
+
+
+class Builder:
+    """ Builder used to construct an object for types defined in the schema """
+
+    def __init__(self, resolver):
+        """
+        @param resolver: A schema object name resolver.
+        @type resolver: L{resolver.Resolver}
+        """
+        self.resolver = resolver
+
+    def build(self, name):
+        """ build a an object for the specified typename as defined in the schema """
+        if isinstance(name, basestring):
+            type = self.resolver.find(name)
+            if type is None:
+                raise TypeNotFound(name)
+        else:
+            type = name
+        cls = type.name
+        if type.mixed():
+            data = Factory.property(cls)
+        else:
+            data = Factory.object(cls)
+        resolved = type.resolve()
+        md = data.__metadata__
+        md.sxtype = resolved
+        md.ordering = self.ordering(resolved)
+        history = []
+        self.add_attributes(data, resolved)
+        for child, ancestry in type.children():
+            if self.skip_child(child, ancestry):
+                continue
+            self.process(data, child, history[:])
+        return data
+
+    def process(self, data, type, history):
+        """ process the specified type then process its children """
+        if type in history:
+            return
+        if type.enum():
+            return
+        history.append(type)
+        resolved = type.resolve()
+        value = None
+        if type.multi_occurrence():
+            value = []
+        else:
+            if len(resolved) > 0:
+                if resolved.mixed():
+                    value = Factory.property(resolved.name)
+                    md = value.__metadata__
+                    md.sxtype = resolved
+                else:
+                    value = Factory.object(resolved.name)
+                    md = value.__metadata__
+                    md.sxtype = resolved
+                    md.ordering = self.ordering(resolved)
+        setattr(data, type.name, value)
+        if value is not None:
+            data = value
+        if not isinstance(data, list):
+            self.add_attributes(data, resolved)
+            for child, ancestry in resolved.children():
+                if self.skip_child(child, ancestry):
+                    continue
+                self.process(data, child, history[:])
+
+    def add_attributes(self, data, type):
+        """ add required attributes """
+        for attr, ancestry in type.attributes():
+            name = '_%s' % attr.name
+            value = attr.get_default()
+            setattr(data, name, value)
+
+    def skip_child(self, child, ancestry):
+        """ get whether or not to skip the specified child """
+        if child.any(): return True
+        for x in ancestry:
+            if x.choice():
+                return True
+        return False
+
+    def ordering(self, type):
+        """ get the ordering """
+        result = []
+        for child, ancestry in type.resolve():
+            name = child.name
+            if child.name is None:
+                continue
+            if child.isattr():
+                name = '_%s' % child.name
+            result.append(name)
+        return result
diff --git a/suds/cache.py b/suds/cache.py
new file mode 100644
index 00000000..e156d191
--- /dev/null
+++ b/suds/cache.py
@@ -0,0 +1,301 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Contains basic caching classes.
+"""
+
+import suds
+from suds.transport import *
+from suds.sax.parser import Parser
+from suds.sax.element import Element
+
+from datetime import datetime as dt
+from datetime import timedelta
+import os
+from tempfile import gettempdir as tmp
+try:
+    import cPickle as pickle
+except Exception:
+    import pickle
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Cache:
+    """
+    An object object cache.
+    """
+
+    def get(self, id):
+        """
+        Get a object from the cache by ID.
+        @param id: The object ID.
+        @type id: str
+        @return: The object, else None
+        @rtype: any
+        """
+        raise Exception('not-implemented')
+
+    def put(self, id, object):
+        """
+        Put a object into the cache.
+        @param id: The object ID.
+        @type id: str
+        @param object: The object to add.
+        @type object: any
+        """
+        raise Exception('not-implemented')
+
+    def purge(self, id):
+        """
+        Purge a object from the cache by id.
+        @param id: A object ID.
+        @type id: str
+        """
+        raise Exception('not-implemented')
+
+    def clear(self):
+        """
+        Clear all objects from the cache.
+        """
+        raise Exception('not-implemented')
+
+
+class NoCache(Cache):
+    """
+    The passthru object cache.
+    """
+
+    def get(self, id):
+        return None
+
+    def put(self, id, object):
+        pass
+
+
+class FileCache(Cache):
+    """
+    A file-based URL cache.
+    @cvar fnprefix: The file name prefix.
+    @type fnsuffix: str
+    @ivar duration: The cached file duration which defines how
+        long the file will be cached.
+    @type duration: (unit, value)
+    @ivar location: The directory for the cached files.
+    @type location: str
+    """
+    fnprefix = 'suds'
+    units = ('months', 'weeks', 'days', 'hours', 'minutes', 'seconds')
+
+    def __init__(self, location=None, **duration):
+        """
+        @param location: The directory for the cached files.
+        @type location: str
+        @param duration: The cached file duration which defines how
+            long the file will be cached.  A duration=0 means forever.
+            The duration may be: (months|weeks|days|hours|minutes|seconds).
+        @type duration: {unit:value}
+        """
+        if location is None:
+            location = os.path.join(tmp(), 'suds')
+        self.location = location
+        self.duration = (None, 0)
+        self.setduration(**duration)
+        self.checkversion()
+
+    def fnsuffix(self):
+        """
+        Get the file name suffix
+        @return: The suffix
+        @rtype: str
+        """
+        return 'gcf'
+
+    def setduration(self, **duration):
+        """
+        Set the caching duration which defines how long the
+        file will be cached.
+        @param duration: The cached file duration which defines how
+            long the file will be cached.  A duration=0 means forever.
+            The duration may be: (months|weeks|days|hours|minutes|seconds).
+        @type duration: {unit:value}
+        """
+        if len(duration) == 1:
+            arg = duration.items()[0]
+            if not arg[0] in self.units:
+                raise Exception('must be: %s' % str(self.units))
+            self.duration = arg
+        return self
+
+    def setlocation(self, location):
+        """
+        Set the location (directory) for the cached files.
+        @param location: The directory for the cached files.
+        @type location: str
+        """
+        self.location = location
+
+    def mktmp(self):
+        """
+        Make the I{location} directory if it doesn't already exits.
+        """
+        try:
+            if not os.path.isdir(self.location):
+                os.makedirs(self.location)
+        except Exception:
+            log.debug(self.location, exc_info=1)
+        return self
+
+    def put(self, id, bfr):
+        try:
+            fn = self.__fn(id)
+            f = self.open(fn, 'wb')
+            try:
+                f.write(bfr)
+            finally:
+                f.close()
+            return bfr
+        except Exception:
+            log.debug(id, exc_info=1)
+            return bfr
+
+    def get(self, id):
+        try:
+            f = self.getf(id)
+            try:
+                return f.read()
+            finally:
+                f.close()
+        except Exception:
+            pass
+
+    def getf(self, id):
+        try:
+            fn = self.__fn(id)
+            self.validate(fn)
+            return self.open(fn, 'rb')
+        except Exception:
+            pass
+
+    def validate(self, fn):
+        """
+        Validate that the file has not expired based on the I{duration}.
+        @param fn: The file name.
+        @type fn: str
+        """
+        if self.duration[1] < 1:
+            return
+        created = dt.fromtimestamp(os.path.getctime(fn))
+        d = {self.duration[0]:self.duration[1]}
+        expired = created + timedelta(**d)
+        if expired < dt.now():
+            log.debug('%s expired, deleted', fn)
+            os.remove(fn)
+
+    def clear(self):
+        for fn in os.listdir(self.location):
+            path = os.path.join(self.location, fn)
+            if os.path.isdir(path):
+                continue
+            if fn.startswith(self.fnprefix):
+                os.remove(path)
+                log.debug('deleted: %s', path)
+
+    def purge(self, id):
+        fn = self.__fn(id)
+        try:
+            os.remove(fn)
+        except Exception:
+            pass
+
+    def open(self, fn, *args):
+        """
+        Open the cache file making sure the directory is created.
+        """
+        self.mktmp()
+        return open(fn, *args)
+
+    def checkversion(self):
+        path = os.path.join(self.location, 'version')
+        try:
+            f = self.open(path)
+            version = f.read()
+            f.close()
+            if version != suds.__version__:
+                raise Exception()
+        except Exception:
+            self.clear()
+            f = self.open(path, 'w')
+            f.write(suds.__version__)
+            f.close()
+
+    def __fn(self, id):
+        name = id
+        suffix = self.fnsuffix()
+        fn = '%s-%s.%s' % (self.fnprefix, name, suffix)
+        return os.path.join(self.location, fn)
+
+
+class DocumentCache(FileCache):
+    """
+    Provides xml document caching.
+    """
+
+    def fnsuffix(self):
+        return 'xml'
+
+    def get(self, id):
+        try:
+            fp = self.getf(id)
+            if fp is None:
+                return None
+            p = Parser()
+            return p.parse(fp)
+        except Exception:
+            self.purge(id)
+
+    def put(self, id, object):
+        if isinstance(object, Element):
+            FileCache.put(self, id, suds.byte_str(str(object)))
+        return object
+
+
+class ObjectCache(FileCache):
+    """
+    Provides pickled object caching.
+    @cvar protocol: The pickling protocol.
+    @type protocol: int
+    """
+    protocol = 2
+
+    def fnsuffix(self):
+        return 'px'
+
+    def get(self, id):
+        try:
+            fp = self.getf(id)
+            if fp is None:
+                return None
+            return pickle.load(fp)
+        except Exception:
+            self.purge(id)
+
+    def put(self, id, object):
+        bfr = pickle.dumps(object, self.protocol)
+        FileCache.put(self, id, bfr)
+        return object
diff --git a/suds/client.py b/suds/client.py
new file mode 100644
index 00000000..d7fda602
--- /dev/null
+++ b/suds/client.py
@@ -0,0 +1,832 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{2nd generation} service proxy provides access to web services.
+See I{README.txt}
+"""
+
+import suds
+from suds import *
+import suds.bindings.binding
+from suds.builder import Builder
+from suds.cache import ObjectCache
+import suds.metrics as metrics
+from suds.options import Options
+from suds.plugin import PluginContainer
+from suds.properties import Unskin
+from suds.reader import DefinitionsReader
+from suds.resolver import PathResolver
+from suds.sax.document import Document
+from suds.sax.parser import Parser
+from suds.servicedefinition import ServiceDefinition
+from suds.transport import TransportError, Request
+from suds.transport.https import HttpAuthenticated
+from suds.umx.basic import Basic as UmxBasic
+from suds.wsdl import Definitions
+import sudsobject
+
+from cookielib import CookieJar
+from copy import deepcopy
+import httplib
+from urlparse import urlparse
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Client(UnicodeMixin):
+    """
+    A lightweight web services client.
+    I{(2nd generation)} API.
+    @ivar wsdl: The WSDL object.
+    @type wsdl:L{Definitions}
+    @ivar service: The service proxy used to invoke operations.
+    @type service: L{Service}
+    @ivar factory: The factory used to create objects.
+    @type factory: L{Factory}
+    @ivar sd: The service definition
+    @type sd: L{ServiceDefinition}
+    @ivar messages: The last sent/received messages.
+    @type messages: str[2]
+    """
+    @classmethod
+    def items(cls, sobject):
+        """
+        Extract the I{items} from a suds object much like the
+        items() method works on I{dict}.
+        @param sobject: A suds object
+        @type sobject: L{Object}
+        @return: A list of items contained in I{sobject}.
+        @rtype: [(key, value),...]
+        """
+        return sudsobject.items(sobject)
+
+    @classmethod
+    def dict(cls, sobject):
+        """
+        Convert a sudsobject into a dictionary.
+        @param sobject: A suds object
+        @type sobject: L{Object}
+        @return: A python dictionary containing the
+            items contained in I{sobject}.
+        @rtype: dict
+        """
+        return sudsobject.asdict(sobject)
+
+    @classmethod
+    def metadata(cls, sobject):
+        """
+        Extract the metadata from a suds object.
+        @param sobject: A suds object
+        @type sobject: L{Object}
+        @return: The object's metadata
+        @rtype: L{sudsobject.Metadata}
+        """
+        return sobject.__metadata__
+
+    def __init__(self, url, **kwargs):
+        """
+        @param url: The URL for the WSDL.
+        @type url: str
+        @param kwargs: keyword arguments.
+        @see: L{Options}
+        """
+        options = Options()
+        options.transport = HttpAuthenticated()
+        self.options = options
+        if "cache" not in kwargs:
+            kwargs["cache"] = ObjectCache(days=1)
+        self.set_options(**kwargs)
+        reader = DefinitionsReader(options, Definitions)
+        self.wsdl = reader.open(url)
+        plugins = PluginContainer(options.plugins)
+        plugins.init.initialized(wsdl=self.wsdl)
+        self.factory = Factory(self.wsdl)
+        self.service = ServiceSelector(self, self.wsdl.services)
+        self.sd = []
+        for s in self.wsdl.services:
+            sd = ServiceDefinition(self.wsdl, s)
+            self.sd.append(sd)
+        self.messages = dict(tx=None, rx=None)
+
+    def set_options(self, **kwargs):
+        """
+        Set options.
+        @param kwargs: keyword arguments.
+        @see: L{Options}
+        """
+        p = Unskin(self.options)
+        p.update(kwargs)
+
+    def add_prefix(self, prefix, uri):
+        """
+        Add I{static} mapping of an XML namespace prefix to a namespace.
+        This is useful for cases when a wsdl and referenced schemas make heavy
+        use of namespaces and those namespaces are subject to change.
+        @param prefix: An XML namespace prefix.
+        @type prefix: str
+        @param uri: An XML namespace URI.
+        @type uri: str
+        @raise Exception: when prefix is already mapped.
+        """
+        root = self.wsdl.root
+        mapped = root.resolvePrefix(prefix, None)
+        if mapped is None:
+            root.addPrefix(prefix, uri)
+            return
+        if mapped[1] != uri:
+            raise Exception('"%s" already mapped as "%s"' % (prefix, mapped))
+
+    def clone(self):
+        """
+        Get a shallow clone of this object.
+        The clone only shares the WSDL.  All other attributes are
+        unique to the cloned object including options.
+        @return: A shallow clone.
+        @rtype: L{Client}
+        """
+        class Uninitialized(Client):
+            def __init__(self):
+                pass
+        clone = Uninitialized()
+        clone.options = Options()
+        cp = Unskin(clone.options)
+        mp = Unskin(self.options)
+        cp.update(deepcopy(mp))
+        clone.wsdl = self.wsdl
+        clone.factory = self.factory
+        clone.service = ServiceSelector(clone, self.wsdl.services)
+        clone.sd = self.sd
+        clone.messages = dict(tx=None, rx=None)
+        return clone
+
+    def __unicode__(self):
+        s = ['\n']
+        s.append('Suds ( https://fedorahosted.org/suds/ )')
+        s.append('  version: %s' % suds.__version__)
+        if ( suds.__build__ ):
+            s.append('  build: %s' % suds.__build__)
+        for sd in self.sd:
+            s.append('\n\n%s' % unicode(sd))
+        return ''.join(s)
+
+
+class Factory:
+    """
+    A factory for instantiating types defined in the wsdl
+    @ivar resolver: A schema type resolver.
+    @type resolver: L{PathResolver}
+    @ivar builder: A schema object builder.
+    @type builder: L{Builder}
+    """
+
+    def __init__(self, wsdl):
+        """
+        @param wsdl: A schema object.
+        @type wsdl: L{wsdl.Definitions}
+        """
+        self.wsdl = wsdl
+        self.resolver = PathResolver(wsdl)
+        self.builder = Builder(self.resolver)
+
+    def create(self, name):
+        """
+        create a WSDL type by name
+        @param name: The name of a type defined in the WSDL.
+        @type name: str
+        @return: The requested object.
+        @rtype: L{Object}
+        """
+        timer = metrics.Timer()
+        timer.start()
+        type = self.resolver.find(name)
+        if type is None:
+            raise TypeNotFound(name)
+        if type.enum():
+            result = sudsobject.Factory.object(name)
+            for e, a in type.children():
+                setattr(result, e.name, e.name)
+        else:
+            try:
+                result = self.builder.build(type)
+            except Exception, e:
+                log.error("create '%s' failed", name, exc_info=True)
+                raise BuildError(name, e)
+        timer.stop()
+        metrics.log.debug('%s created: %s', name, timer)
+        return result
+
+    def separator(self, ps):
+        """
+        Set the path separator.
+        @param ps: The new path separator.
+        @type ps: char
+        """
+        self.resolver = PathResolver(self.wsdl, ps)
+
+
+class ServiceSelector:
+    """
+    The B{service} selector is used to select a web service.
+    In most cases, the wsdl only defines (1) service in which access
+    by subscript is passed through to a L{PortSelector}.  This is also the
+    behavior when a I{default} service has been specified.  In cases
+    where multiple services have been defined and no default has been
+    specified, the service is found by name (or index) and a L{PortSelector}
+    for the service is returned.  In all cases, attribute access is
+    forwarded to the L{PortSelector} for either the I{first} service or the
+    I{default} service (when specified).
+    @ivar __client: A suds client.
+    @type __client: L{Client}
+    @ivar __services: A list of I{wsdl} services.
+    @type __services: list
+    """
+    def __init__(self, client, services):
+        """
+        @param client: A suds client.
+        @type client: L{Client}
+        @param services: A list of I{wsdl} services.
+        @type services: list
+        """
+        self.__client = client
+        self.__services = services
+
+    def __getattr__(self, name):
+        """
+        Request to access an attribute is forwarded to the
+        L{PortSelector} for either the I{first} service or the
+        I{default} service (when specified).
+        @param name: The name of a method.
+        @type name: str
+        @return: A L{PortSelector}.
+        @rtype: L{PortSelector}.
+        """
+        default = self.__ds()
+        if default is None:
+            port = self.__find(0)
+        else:
+            port = default
+        return getattr(port, name)
+
+    def __getitem__(self, name):
+        """
+        Provides selection of the I{service} by name (string) or
+        index (integer).  In cases where only (1) service is defined
+        or a I{default} has been specified, the request is forwarded
+        to the L{PortSelector}.
+        @param name: The name (or index) of a service.
+        @type name: (int|str)
+        @return: A L{PortSelector} for the specified service.
+        @rtype: L{PortSelector}.
+        """
+        if len(self.__services) == 1:
+            port = self.__find(0)
+            return port[name]
+        default = self.__ds()
+        if default is not None:
+            port = default
+            return port[name]
+        return self.__find(name)
+
+    def __find(self, name):
+        """
+        Find a I{service} by name (string) or index (integer).
+        @param name: The name (or index) of a service.
+        @type name: (int|str)
+        @return: A L{PortSelector} for the found service.
+        @rtype: L{PortSelector}.
+        """
+        service = None
+        if not len(self.__services):
+            raise Exception, 'No services defined'
+        if isinstance(name, int):
+            try:
+                service = self.__services[name]
+                name = service.name
+            except IndexError:
+                raise ServiceNotFound, 'at [%d]' % name
+        else:
+            for s in self.__services:
+                if name == s.name:
+                    service = s
+                    break
+        if service is None:
+            raise ServiceNotFound, name
+        return PortSelector(self.__client, service.ports, name)
+
+    def __ds(self):
+        """
+        Get the I{default} service if defined in the I{options}.
+        @return: A L{PortSelector} for the I{default} service.
+        @rtype: L{PortSelector}.
+        """
+        ds = self.__client.options.service
+        if ds is None:
+            return None
+        else:
+            return self.__find(ds)
+
+
+class PortSelector:
+    """
+    The B{port} selector is used to select a I{web service} B{port}.
+    In cases where multiple ports have been defined and no default has been
+    specified, the port is found by name (or index) and a L{MethodSelector}
+    for the port is returned.  In all cases, attribute access is
+    forwarded to the L{MethodSelector} for either the I{first} port or the
+    I{default} port (when specified).
+    @ivar __client: A suds client.
+    @type __client: L{Client}
+    @ivar __ports: A list of I{service} ports.
+    @type __ports: list
+    @ivar __qn: The I{qualified} name of the port (used for logging).
+    @type __qn: str
+    """
+    def __init__(self, client, ports, qn):
+        """
+        @param client: A suds client.
+        @type client: L{Client}
+        @param ports: A list of I{service} ports.
+        @type ports: list
+        @param qn: The name of the service.
+        @type qn: str
+        """
+        self.__client = client
+        self.__ports = ports
+        self.__qn = qn
+
+    def __getattr__(self, name):
+        """
+        Request to access an attribute is forwarded to the
+        L{MethodSelector} for either the I{first} port or the
+        I{default} port (when specified).
+        @param name: The name of a method.
+        @type name: str
+        @return: A L{MethodSelector}.
+        @rtype: L{MethodSelector}.
+        """
+        default = self.__dp()
+        if default is None:
+            m = self.__find(0)
+        else:
+            m = default
+        return getattr(m, name)
+
+    def __getitem__(self, name):
+        """
+        Provides selection of the I{port} by name (string) or
+        index (integer).  In cases where only (1) port is defined
+        or a I{default} has been specified, the request is forwarded
+        to the L{MethodSelector}.
+        @param name: The name (or index) of a port.
+        @type name: (int|str)
+        @return: A L{MethodSelector} for the specified port.
+        @rtype: L{MethodSelector}.
+        """
+        default = self.__dp()
+        if default is None:
+            return self.__find(name)
+        else:
+            return default
+
+    def __find(self, name):
+        """
+        Find a I{port} by name (string) or index (integer).
+        @param name: The name (or index) of a port.
+        @type name: (int|str)
+        @return: A L{MethodSelector} for the found port.
+        @rtype: L{MethodSelector}.
+        """
+        port = None
+        if not len(self.__ports):
+            raise Exception, 'No ports defined: %s' % self.__qn
+        if isinstance(name, int):
+            qn = '%s[%d]' % (self.__qn, name)
+            try:
+                port = self.__ports[name]
+            except IndexError:
+                raise PortNotFound, qn
+        else:
+            qn = '.'.join((self.__qn, name))
+            for p in self.__ports:
+                if name == p.name:
+                    port = p
+                    break
+        if port is None:
+            raise PortNotFound, qn
+        qn = '.'.join((self.__qn, port.name))
+        return MethodSelector(self.__client, port.methods, qn)
+
+    def __dp(self):
+        """
+        Get the I{default} port if defined in the I{options}.
+        @return: A L{MethodSelector} for the I{default} port.
+        @rtype: L{MethodSelector}.
+        """
+        dp = self.__client.options.port
+        if dp is None:
+            return None
+        else:
+            return self.__find(dp)
+
+
+class MethodSelector:
+    """
+    The B{method} selector is used to select a B{method} by name.
+    @ivar __client: A suds client.
+    @type __client: L{Client}
+    @ivar __methods: A dictionary of methods.
+    @type __methods: dict
+    @ivar __qn: The I{qualified} name of the method (used for logging).
+    @type __qn: str
+    """
+    def __init__(self, client, methods, qn):
+        """
+        @param client: A suds client.
+        @type client: L{Client}
+        @param methods: A dictionary of methods.
+        @type methods: dict
+        @param qn: The I{qualified} name of the port.
+        @type qn: str
+        """
+        self.__client = client
+        self.__methods = methods
+        self.__qn = qn
+
+    def __getattr__(self, name):
+        """
+        Get a method by name and return it in an I{execution wrapper}.
+        @param name: The name of a method.
+        @type name: str
+        @return: An I{execution wrapper} for the specified method name.
+        @rtype: L{Method}
+        """
+        return self[name]
+
+    def __getitem__(self, name):
+        """
+        Get a method by name and return it in an I{execution wrapper}.
+        @param name: The name of a method.
+        @type name: str
+        @return: An I{execution wrapper} for the specified method name.
+        @rtype: L{Method}
+        """
+        m = self.__methods.get(name)
+        if m is None:
+            qn = '.'.join((self.__qn, name))
+            raise MethodNotFound, qn
+        return Method(self.__client, m)
+
+
+class Method:
+    """
+    The I{method} (namespace) object.
+    @ivar client: A client object.
+    @type client: L{Client}
+    @ivar method: A I{wsdl} method.
+    @type I{wsdl} Method.
+    """
+
+    def __init__(self, client, method):
+        """
+        @param client: A client object.
+        @type client: L{Client}
+        @param method: A I{raw} method.
+        @type I{raw} Method.
+        """
+        self.client = client
+        self.method = method
+
+    def __call__(self, *args, **kwargs):
+        """
+        Invoke the method.
+        """
+        clientclass = self.clientclass(kwargs)
+        client = clientclass(self.client, self.method)
+        try:
+            return client.invoke(args, kwargs)
+        except WebFault, e:
+            if self.faults():
+                raise
+            return (httplib.INTERNAL_SERVER_ERROR, e)
+
+    def faults(self):
+        """ get faults option """
+        return self.client.options.faults
+
+    def clientclass(self, kwargs):
+        """ get soap client class """
+        if SimClient.simulation(kwargs):
+            return SimClient
+        return SoapClient
+
+
+class SoapClient:
+    """
+    A lightweight soap based web client B{**not intended for external use}
+    @ivar service: The target method.
+    @type service: L{Service}
+    @ivar method: A target method.
+    @type method: L{Method}
+    @ivar options: A dictonary of options.
+    @type options: dict
+    @ivar cookiejar: A cookie jar.
+    @type cookiejar: libcookie.CookieJar
+    """
+
+    def __init__(self, client, method):
+        """
+        @param client: A suds client.
+        @type client: L{Client}
+        @param method: A target method.
+        @type method: L{Method}
+        """
+        self.client = client
+        self.method = method
+        self.options = client.options
+        self.cookiejar = CookieJar()
+
+    def invoke(self, args, kwargs):
+        """
+        Send the required soap message to invoke the specified method
+        @param args: A list of args for the method invoked.
+        @type args: list
+        @param kwargs: Named (keyword) args for the method invoked.
+        @type kwargs: dict
+        @return: The result of the method invocation.
+        @rtype: I{builtin}|I{subclass of} L{Object}
+        """
+        timer = metrics.Timer()
+        timer.start()
+        binding = self.method.binding.input
+        soapenv = binding.get_message(self.method, args, kwargs)
+        timer.stop()
+        metrics.log.debug("message for '%s' created: %s", self.method.name,
+            timer)
+        timer.start()
+        result = self.send(soapenv)
+        timer.stop()
+        metrics.log.debug("method '%s' invoked: %s", self.method.name, timer)
+        return result
+
+    def send(self, soapenv):
+        """
+        Send soap message.
+        @param soapenv: A soap envelope to send.
+        @type soapenv: L{Document}
+        @return: The reply to the sent message.
+        @rtype: I{builtin} or I{subclass of} L{Object}
+        """
+        location = self.location()
+        log.debug('sending to (%s)\nmessage:\n%s', location, soapenv)
+        original_soapenv = soapenv
+        plugins = PluginContainer(self.options.plugins)
+        plugins.message.marshalled(envelope=soapenv.root())
+        if self.options.prettyxml:
+            soapenv = soapenv.str()
+        else:
+            soapenv = soapenv.plain()
+        soapenv = soapenv.encode('utf-8')
+        ctx = plugins.message.sending(envelope=soapenv)
+        soapenv = ctx.envelope
+        if self.options.nosend:
+            return RequestContext(self, soapenv, original_soapenv)
+        request = Request(location, soapenv)
+        request.headers = self.headers()
+        try:
+            timer = metrics.Timer()
+            timer.start()
+            reply = self.options.transport.send(request)
+            timer.stop()
+            metrics.log.debug('waited %s on server reply', timer)
+        except TransportError, e:
+            content = e.fp and e.fp.read() or ''
+            return self.process_reply(reply=content, status=e.httpcode,
+                description=tostr(e), original_soapenv=original_soapenv)
+        return self.process_reply(reply=reply.message,
+            original_soapenv=original_soapenv)
+
+    def process_reply(self, reply, status=None, description=None,
+        original_soapenv=None):
+        if status is None:
+            status = httplib.OK
+        if status in (httplib.ACCEPTED, httplib.NO_CONTENT):
+            return
+        failed = True
+        try:
+            if status == httplib.OK:
+                log.debug('HTTP succeeded:\n%s', reply)
+            else:
+                log.debug('HTTP failed - %d - %s:\n%s', status, description,
+                    reply)
+
+            # (todo)
+            #   Consider whether and how to allow plugins to handle error,
+            # httplib.ACCEPTED & httplib.NO_CONTENT replies as well as
+            # successful ones.
+            #                                 (todo) (27.03.2013.) (Jurko)
+            plugins = PluginContainer(self.options.plugins)
+            ctx = plugins.message.received(reply=reply)
+            reply = ctx.reply
+
+            # SOAP standard states that SOAP errors must be accompanied by HTTP
+            # status code 500 - internal server error:
+            #
+            # From SOAP 1.1 Specification:
+            #   In case of a SOAP error while processing the request, the SOAP
+            # HTTP server MUST issue an HTTP 500 "Internal Server Error"
+            # response and include a SOAP message in the response containing a
+            # SOAP Fault element (see section 4.4) indicating the SOAP
+            # processing error.
+            #
+            # From WS-I Basic profile:
+            #   An INSTANCE MUST use a "500 Internal Server Error" HTTP status
+            # code if the response message is a SOAP Fault.
+            replyroot = None
+            if status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+                replyroot = _parse(reply)
+                plugins.message.parsed(reply=replyroot)
+                fault = self.get_fault(replyroot)
+                if fault:
+                    if status != httplib.INTERNAL_SERVER_ERROR:
+                        log.warn("Web service reported a SOAP processing "
+                            "fault using an unexpected HTTP status code %d. "
+                            "Reporting as an internal server error.", status)
+                    if self.options.faults:
+                        raise WebFault(fault, replyroot)
+                    return (httplib.INTERNAL_SERVER_ERROR, fault)
+            if status != httplib.OK:
+                if self.options.faults:
+                    # (todo)
+                    #   Use a more specific exception class here.
+                    #                         (27.03.2013.) (Jurko)
+                    raise Exception((status, description))
+                return (status, description)
+
+            if self.options.retxml:
+                failed = False
+                return reply
+
+            result = replyroot and self.method.binding.output.get_reply(
+                self.method, replyroot)
+            ctx = plugins.message.unmarshalled(reply=result)
+            result = ctx.reply
+            failed = False
+            if self.options.faults:
+                return result
+            return (httplib.OK, result)
+        finally:
+            if failed and original_soapenv:
+                log.error(original_soapenv)
+
+    def get_fault(self, replyroot):
+        """Extract fault information from the specified SOAP reply.
+
+          Returns an I{unmarshalled} fault L{Object} or None in case the given
+        XML document does not contain the SOAP <Fault> element.
+
+        @param replyroot: A SOAP reply message root XML element or None.
+        @type replyroot: L{Element}
+        @return: A fault object.
+        @rtype: L{Object}
+        """
+        envns = suds.bindings.binding.envns
+        soapenv = replyroot and replyroot.getChild('Envelope', envns)
+        soapbody = soapenv and soapenv.getChild('Body', envns)
+        fault = soapbody and soapbody.getChild('Fault', envns)
+        return fault is not None and UmxBasic().process(fault)
+
+    def headers(self):
+        """
+        Get HTTP headers or the HTTP/HTTPS request.
+        @return: A dictionary of header/values.
+        @rtype: dict
+        """
+        action = self.method.soap.action
+        if isinstance(action, unicode):
+            action = action.encode('utf-8')
+        stock = {'Content-Type':'text/xml; charset=utf-8', 'SOAPAction':action}
+        result = dict(stock, **self.options.headers)
+        log.debug('headers = %s', result)
+        return result
+
+    def location(self):
+        """
+        Returns the SOAP request's target location URL.
+
+        """
+        return Unskin(self.options).get('location', self.method.location)
+
+
+class SimClient(SoapClient):
+    """
+    Loopback client used for message/reply simulation.
+    """
+
+    injkey = '__inject'
+
+    @classmethod
+    def simulation(cls, kwargs):
+        """ get whether loopback has been specified in the I{kwargs}. """
+        return kwargs.has_key(SimClient.injkey)
+
+    def invoke(self, args, kwargs):
+        """
+        Send the required soap message to invoke the specified method
+        @param args: A list of args for the method invoked.
+        @type args: list
+        @param kwargs: Named (keyword) args for the method invoked.
+        @type kwargs: dict
+        @return: The result of the method invocation.
+        @rtype: I{builtin} or I{subclass of} L{Object}
+        """
+        simulation = kwargs[self.injkey]
+        msg = simulation.get('msg')
+        if msg is not None:
+            assert msg.__class__ is suds.byte_str_class
+            return self.send(_parse(msg))
+        msg = self.method.binding.input.get_message(self.method, args, kwargs)
+        log.debug('inject (simulated) send message:\n%s', msg)
+        reply = simulation.get('reply')
+        if reply is not None:
+            assert reply.__class__ is suds.byte_str_class
+            status = simulation.get('status')
+            description=simulation.get('description')
+            if description is None:
+                description = 'injected reply'
+            return self.process_reply(reply=reply, status=status,
+                description=description, original_soapenv=msg)
+        raise Exception('reply or msg injection parameter expected');
+
+
+class RequestContext:
+    """
+    A request context.
+    Returned when the ''nosend'' options is specified. Allows the caller to
+    take care of sending the request himself and simply return the reply data
+    for further processing.
+    @ivar client: The suds client.
+    @type client: L{Client}
+    @ivar envelope: The request SOAP envelope.
+    @type envelope: str
+    @ivar original_envelope: The original request SOAP envelope before plugin
+                             processing.
+    @type original_envelope: str
+    """
+
+    def __init__(self, client, envelope, original_envelope):
+        """
+        @param client: The suds client.
+        @type client: L{Client}
+        @param envelope: The request SOAP envelope.
+        @type envelope: str
+        @param original_envelope: The original request SOAP envelope before
+                                  plugin processing.
+        @type original_envelope: str
+        """
+        self.client = client
+        self.envelope = envelope
+        self.original_envelope = original_envelope
+
+    def process_reply(self, reply, status=None, description=None):
+        """
+        Re-entry for processing a successful reply.
+        @param reply: The reply SOAP envelope.
+        @type reply: str
+        @param status: The HTTP status code
+        @type status: int
+        @param description: Additional status description.
+        @type description: str
+        @return: The returned value for the invoked method.
+        @return: The result of the method invocation.
+        @rtype: I{builtin}|I{subclass of} L{Object}
+        """
+        return self.client.process_reply(reply=reply, status=status,
+            description=description, original_soapenv=self.original_envelope)
+
+
+def _parse(string):
+    """
+    Parses the given XML document content and returns the resulting root XML
+    element node. Returns None if the given XML content is empty.
+    @param string: XML document content to parse.
+    @type string: str
+    @return: Resulting root XML element node or None.
+    @rtype: L{Element}
+    """
+    if len(string) > 0:
+        return Parser().parse(string=string)
diff --git a/suds/metrics.py b/suds/metrics.py
new file mode 100644
index 00000000..9b15f18a
--- /dev/null
+++ b/suds/metrics.py
@@ -0,0 +1,63 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{metrics} module defines classes and other resources
+designed for collecting and reporting performance metrics.
+"""
+
+import time
+from suds import *
+from math import modf
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Timer:
+
+    def __init__(self):
+        self.started = 0
+        self.stopped = 0
+
+    def start(self):
+        self.started = time.time()
+        self.stopped = 0
+        return self
+
+    def stop(self):
+        if self.started > 0:
+            self.stopped = time.time()
+        return self
+
+    def duration(self):
+        return ( self.stopped - self.started )
+
+    def __str__(self):
+        if self.started == 0:
+            return 'not-running'
+        if self.started > 0 and self.stopped == 0:
+            return 'started: %d (running)' % self.started
+        duration = self.duration()
+        jmod = ( lambda m : (m[1], m[0]*1000) )
+        if duration < 1:
+            ms = (duration*1000)
+            return '%d (ms)' % ms
+        if duration < 60:
+            m = modf(duration)
+            return '%d.%.3d (seconds)' % jmod(m)
+        m = modf(duration/60)
+        return '%d.%.3d (minutes)' % jmod(m)
diff --git a/suds/mx/__init__.py b/suds/mx/__init__.py
new file mode 100644
index 00000000..719e52df
--- /dev/null
+++ b/suds/mx/__init__.py
@@ -0,0 +1,59 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides modules containing classes to support
+marshalling (XML).
+"""
+
+from suds.sudsobject import Object
+
+
+class Content(Object):
+    """
+    Marshaller Content.
+    @ivar tag: The content tag.
+    @type tag: str
+    @ivar value: The content's value.
+    @type value: I{any}
+    """
+
+    extensions = []
+
+    def __init__(self, tag=None, value=None, **kwargs):
+        """
+        @param tag: The content tag.
+        @type tag: str
+        @param value: The content's value.
+        @type value: I{any}
+        """
+        Object.__init__(self)
+        self.tag = tag
+        self.value = value
+        for k,v in kwargs.items():
+            setattr(self, k, v)
+
+    def __getattr__(self, name):
+        if name not in self.__dict__:
+            if name in self.extensions:
+                v = None
+                setattr(self, name, v)
+            else:
+                raise AttributeError, \
+                    'Content has no attribute %s' % name
+        else:
+            v = self.__dict__[name]
+        return v
diff --git a/suds/mx/appender.py b/suds/mx/appender.py
new file mode 100644
index 00000000..f900338f
--- /dev/null
+++ b/suds/mx/appender.py
@@ -0,0 +1,302 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides appender classes for I{marshalling}.
+"""
+
+from suds import *
+from suds.mx import *
+from suds.sudsobject import footprint
+from suds.sudsobject import Object, Property
+from suds.sax.element import Element
+from suds.sax.text import Text
+
+
+class Matcher:
+    """
+    Appender matcher.
+    @ivar cls: A class object.
+    @type cls: I{classobj}
+    """
+
+    def __init__(self, cls):
+        """
+        @param cls: A class object.
+        @type cls: I{classobj}
+        """
+        self.cls = cls
+
+    def __eq__(self, x):
+        if self.cls is None:
+            return x is None
+        return isinstance(x, self.cls)
+
+
+class ContentAppender:
+    """
+    Appender used to add content to marshalled objects.
+    @ivar default: The default appender.
+    @type default: L{Appender}
+    @ivar appenders: A I{table} of appenders mapped by class.
+    @type appenders: I{table}
+    """
+
+    def __init__(self, marshaller):
+        """
+        @param marshaller: A marshaller.
+        @type marshaller: L{suds.mx.core.Core}
+        """
+        self.default = PrimativeAppender(marshaller)
+        self.appenders = (
+            (Matcher(None), NoneAppender(marshaller)),
+            (Matcher(null), NoneAppender(marshaller)),
+            (Matcher(Property), PropertyAppender(marshaller)),
+            (Matcher(Object), ObjectAppender(marshaller)),
+            (Matcher(Element), ElementAppender(marshaller)),
+            (Matcher(Text), TextAppender(marshaller)),
+            (Matcher(list), ListAppender(marshaller)),
+            (Matcher(tuple), ListAppender(marshaller)),
+            (Matcher(dict), DictAppender(marshaller)))
+
+    def append(self, parent, content):
+        """
+        Select an appender and append the content to parent.
+        @param parent: A parent node.
+        @type parent: L{Element}
+        @param content: The content to append.
+        @type content: L{Content}
+        """
+        appender = self.default
+        for matcher, candidate_appender in self.appenders:
+            if matcher == content.value:
+                appender = candidate_appender
+                break
+        appender.append(parent, content)
+
+
+class Appender:
+    """
+    An appender used by the marshaller to append content.
+    @ivar marshaller: A marshaller.
+    @type marshaller: L{suds.mx.core.Core}
+    """
+
+    def __init__(self, marshaller):
+        """
+        @param marshaller: A marshaller.
+        @type marshaller: L{suds.mx.core.Core}
+        """
+        self.marshaller  = marshaller
+
+    def node(self, content):
+        """
+        Create and return an XML node that is qualified
+        using the I{type}.  Also, make sure all referenced namespace
+        prefixes are declared.
+        @param content: The content for which processing has ended.
+        @type content: L{Object}
+        @return: A new node.
+        @rtype: L{Element}
+        """
+        return self.marshaller.node(content)
+
+    def setnil(self, node, content):
+        """
+        Set the value of the I{node} to nill.
+        @param node: A I{nil} node.
+        @type node: L{Element}
+        @param content: The content for which processing has ended.
+        @type content: L{Object}
+        """
+        self.marshaller.setnil(node, content)
+
+    def setdefault(self, node, content):
+        """
+        Set the value of the I{node} to a default value.
+        @param node: A I{nil} node.
+        @type node: L{Element}
+        @param content: The content for which processing has ended.
+        @type content: L{Object}
+        @return: The default.
+        """
+        return self.marshaller.setdefault(node, content)
+
+    def optional(self, content):
+        """
+        Get whether the specified content is optional.
+        @param content: The content which to check.
+        @type content: L{Content}
+        """
+        return self.marshaller.optional(content)
+
+    def suspend(self, content):
+        """
+        Notify I{marshaller} that appending this content has suspended.
+        @param content: The content for which processing has been suspended.
+        @type content: L{Object}
+        """
+        self.marshaller.suspend(content)
+
+    def resume(self, content):
+        """
+        Notify I{marshaller} that appending this content has resumed.
+        @param content: The content for which processing has been resumed.
+        @type content: L{Object}
+        """
+        self.marshaller.resume(content)
+
+    def append(self, parent, content):
+        """
+        Append the specified L{content} to the I{parent}.
+        @param content: The content to append.
+        @type content: L{Object}
+        """
+        self.marshaller.append(parent, content)
+
+
+class PrimativeAppender(Appender):
+    """
+    An appender for python I{primative} types.
+    """
+
+    def append(self, parent, content):
+        if content.tag.startswith('_'):
+            attr = content.tag[1:]
+            value = tostr(content.value)
+            if value:
+                parent.set(attr, value)
+        else:
+            child = self.node(content)
+            child.setText(tostr(content.value))
+            parent.append(child)
+
+
+class NoneAppender(Appender):
+    """
+    An appender for I{None} values.
+    """
+
+    def append(self, parent, content):
+        child = self.node(content)
+        default = self.setdefault(child, content)
+        if default is None:
+            self.setnil(child, content)
+        parent.append(child)
+
+
+class PropertyAppender(Appender):
+    """
+    A L{Property} appender.
+    """
+
+    def append(self, parent, content):
+        p = content.value
+        child = self.node(content)
+        child.setText(p.get())
+        parent.append(child)
+        for item in p.items():
+            cont = Content(tag=item[0], value=item[1])
+            Appender.append(self, child, cont)
+
+
+class ObjectAppender(Appender):
+    """
+    An L{Object} appender.
+    """
+
+    def append(self, parent, content):
+        object = content.value
+        if self.optional(content) and footprint(object) == 0:
+            return
+        child = self.node(content)
+        parent.append(child)
+        for item in object:
+            cont = Content(tag=item[0], value=item[1])
+            Appender.append(self, child, cont)
+
+
+class DictAppender(Appender):
+    """
+    An python I{dict} appender.
+    """
+
+    def append(self, parent, content):
+        d = content.value
+        if self.optional(content) and len(d) == 0:
+            return
+        child = self.node(content)
+        parent.append(child)
+        for item in d.items():
+            cont = Content(tag=item[0], value=item[1])
+            Appender.append(self, child, cont)
+
+
+class ElementWrapper(Element):
+    """
+    Element wrapper.
+    """
+
+    def __init__(self, content):
+        Element.__init__(self, content.name, content.parent)
+        self.__content = content
+
+    def str(self, indent=0):
+        return self.__content.str(indent)
+
+
+class ElementAppender(Appender):
+    """
+    An appender for I{Element} types.
+    """
+
+    def append(self, parent, content):
+        if content.tag.startswith('_'):
+            raise Exception('raw XML not valid as attribute value')
+        child = ElementWrapper(content.value)
+        parent.append(child)
+
+
+class ListAppender(Appender):
+    """
+    A list/tuple appender.
+    """
+
+    def append(self, parent, content):
+        collection = content.value
+        if len(collection):
+            self.suspend(content)
+            for item in collection:
+                cont = Content(tag=content.tag, value=item)
+                Appender.append(self, parent, cont)
+            self.resume(content)
+
+
+class TextAppender(Appender):
+    """
+    An appender for I{Text} values.
+    """
+
+    def append(self, parent, content):
+        if content.tag.startswith('_'):
+            attr = content.tag[1:]
+            value = tostr(content.value)
+            if value:
+                parent.set(attr, value)
+        else:
+            child = self.node(content)
+            child.setText(content.value)
+            parent.append(child)
diff --git a/suds/mx/basic.py b/suds/mx/basic.py
new file mode 100644
index 00000000..b2de1611
--- /dev/null
+++ b/suds/mx/basic.py
@@ -0,0 +1,45 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides basic I{marshaller} classes.
+"""
+
+from suds import *
+from suds.mx import *
+from suds.mx.core import Core
+
+
+class Basic(Core):
+    """
+    A I{basic} (untyped) marshaller.
+    """
+
+    def process(self, value, tag=None):
+        """
+        Process (marshal) the tag with the specified value using the
+        optional type information.
+        @param value: The value (content) of the XML node.
+        @type value: (L{Object}|any)
+        @param tag: The (optional) tag name for the value.  The default is
+            value.__class__.__name__
+        @type tag: str
+        @return: An xml node.
+        @rtype: L{Element}
+        """
+        content = Content(tag=tag, value=value)
+        result = Core.process(self, content)
+        return result
diff --git a/suds/mx/core.py b/suds/mx/core.py
new file mode 100644
index 00000000..b7fe24c1
--- /dev/null
+++ b/suds/mx/core.py
@@ -0,0 +1,154 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides I{marshaller} core classes.
+"""
+
+from suds import *
+from suds.mx import *
+from suds.mx.appender import ContentAppender
+from suds.sax.element import Element
+from suds.sax.document import Document
+from suds.sudsobject import Property
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Core:
+    """
+    An I{abstract} marshaller.  This class implement the core
+    functionality of the marshaller.
+    @ivar appender: A content appender.
+    @type appender: L{ContentAppender}
+    """
+
+    def __init__(self):
+        """
+        """
+        self.appender = ContentAppender(self)
+
+    def process(self, content):
+        """
+        Process (marshal) the tag with the specified value using the
+        optional type information.
+        @param content: The content to process.
+        @type content: L{Object}
+        """
+        log.debug('processing:\n%s', content)
+        self.reset()
+        if content.tag is None:
+            content.tag = content.value.__class__.__name__
+        document = Document()
+        if isinstance(content.value, Property):
+            root = self.node(content)
+        self.append(document, content)
+        return document.root()
+
+    def append(self, parent, content):
+        """
+        Append the specified L{content} to the I{parent}.
+        @param parent: The parent node to append to.
+        @type parent: L{Element}
+        @param content: The content to append.
+        @type content: L{Object}
+        """
+        log.debug('appending parent:\n%s\ncontent:\n%s', parent, content)
+        if self.start(content):
+            self.appender.append(parent, content)
+            self.end(parent, content)
+
+    def reset(self):
+        """
+        Reset the marshaller.
+        """
+        pass
+
+    def node(self, content):
+        """
+        Create and return an XML node.
+        @param content: The content for which processing has been suspended.
+        @type content: L{Object}
+        @return: An element.
+        @rtype: L{Element}
+        """
+        return Element(content.tag)
+
+    def start(self, content):
+        """
+        Appending this content has started.
+        @param content: The content for which processing has started.
+        @type content: L{Content}
+        @return: True to continue appending
+        @rtype: boolean
+        """
+        return True
+
+    def suspend(self, content):
+        """
+        Appending this content has suspended.
+        @param content: The content for which processing has been suspended.
+        @type content: L{Content}
+        """
+        pass
+
+    def resume(self, content):
+        """
+        Appending this content has resumed.
+        @param content: The content for which processing has been resumed.
+        @type content: L{Content}
+        """
+        pass
+
+    def end(self, parent, content):
+        """
+        Appending this content has ended.
+        @param parent: The parent node ending.
+        @type parent: L{Element}
+        @param content: The content for which processing has ended.
+        @type content: L{Content}
+        """
+        pass
+
+    def setnil(self, node, content):
+        """
+        Set the value of the I{node} to nill.
+        @param node: A I{nil} node.
+        @type node: L{Element}
+        @param content: The content to set nil.
+        @type content: L{Content}
+        """
+        pass
+
+    def setdefault(self, node, content):
+        """
+        Set the value of the I{node} to a default value.
+        @param node: A I{nil} node.
+        @type node: L{Element}
+        @param content: The content to set the default value.
+        @type content: L{Content}
+        @return: The default.
+        """
+        pass
+
+    def optional(self, content):
+        """
+        Get whether the specified content is optional.
+        @param content: The content which to check.
+        @type content: L{Content}
+        """
+        return False
diff --git a/suds/mx/encoded.py b/suds/mx/encoded.py
new file mode 100644
index 00000000..ec095368
--- /dev/null
+++ b/suds/mx/encoded.py
@@ -0,0 +1,131 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides encoded I{marshaller} classes.
+"""
+
+from suds import *
+from suds.mx import *
+from suds.mx.literal import Literal
+from suds.mx.typer import Typer
+from suds.sudsobject import Factory, Object
+from suds.xsd.query import TypeQuery
+
+
+#
+# Add encoded extensions
+# aty = The soap (section 5) encoded array type.
+#
+Content.extensions.append('aty')
+
+
+class Encoded(Literal):
+    """
+    A SOAP section (5) encoding marshaller.
+    This marshaller supports rpc/encoded soap styles.
+    """
+
+    def start(self, content):
+        #
+        # For soap encoded arrays, the 'aty' (array type) information
+        # is extracted and added to the 'content'.  Then, the content.value
+        # is replaced with an object containing an 'item=[]' attribute
+        # containing values that are 'typed' suds objects.
+        #
+        start = Literal.start(self, content)
+        if start and isinstance(content.value, (list,tuple)):
+            resolved = content.type.resolve()
+            for c in resolved:
+                if hasattr(c[0], 'aty'):
+                    content.aty = (content.tag, c[0].aty)
+                    self.cast(content)
+                    break
+        return start
+
+    def end(self, parent, content):
+        #
+        # For soap encoded arrays, the soapenc:arrayType attribute is
+        # added with proper type and size information.
+        # Eg: soapenc:arrayType="xs:int[3]"
+        #
+        Literal.end(self, parent, content)
+        if content.aty is None:
+            return
+        tag, aty = content.aty
+        ns0 = ('at0', aty[1])
+        ns1 = ('at1', 'http://schemas.xmlsoap.org/soap/encoding/')
+        array = content.value.item
+        child = parent.getChild(tag)
+        child.addPrefix(ns0[0], ns0[1])
+        child.addPrefix(ns1[0], ns1[1])
+        name = '%s:arrayType' % ns1[0]
+        value = '%s:%s[%d]' % (ns0[0], aty[0], len(array))
+        child.set(name, value)
+
+    def encode(self, node, content):
+        if content.type.any():
+            Typer.auto(node, content.value)
+            return
+        if content.real.any():
+            Typer.auto(node, content.value)
+            return
+        ns = None
+        name = content.real.name
+        if self.xstq:
+            ns = content.real.namespace()
+        Typer.manual(node, name, ns)
+
+    def cast(self, content):
+        """
+        Cast the I{untyped} list items found in content I{value}.
+        Each items contained in the list is checked for XSD type information.
+        Items (values) that are I{untyped}, are replaced with suds objects and
+        type I{metadata} is added.
+        @param content: The content holding the collection.
+        @type content: L{Content}
+        @return: self
+        @rtype: L{Encoded}
+        """
+        aty = content.aty[1]
+        resolved = content.type.resolve()
+        array = Factory.object(resolved.name)
+        array.item = []
+        query = TypeQuery(aty)
+        ref = query.execute(self.schema)
+        if ref is None:
+            raise TypeNotFound(qref)
+        for x in content.value:
+            if isinstance(x, (list, tuple)):
+                array.item.append(x)
+                continue
+            if isinstance(x, Object):
+                md = x.__metadata__
+                md.sxtype = ref
+                array.item.append(x)
+                continue
+            if isinstance(x, dict):
+                x = Factory.object(ref.name, x)
+                md = x.__metadata__
+                md.sxtype = ref
+                array.item.append(x)
+                continue
+            x = Factory.property(ref.name, x)
+            md = x.__metadata__
+            md.sxtype = ref
+            array.item.append(x)
+        content.value = array
+        return self
diff --git a/suds/mx/literal.py b/suds/mx/literal.py
new file mode 100644
index 00000000..4b3480f7
--- /dev/null
+++ b/suds/mx/literal.py
@@ -0,0 +1,287 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides literal I{marshaller} classes.
+"""
+
+from suds import *
+from suds.mx import *
+from suds.mx.core import Core
+from suds.mx.typer import Typer
+from suds.resolver import GraphResolver, Frame
+from suds.sax.element import Element
+from suds.sudsobject import Factory
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+#
+# Add typed extensions
+# type = The expected xsd type
+# real = The 'true' XSD type
+# ancestry = The 'type' ancestry
+#
+Content.extensions.append('type')
+Content.extensions.append('real')
+Content.extensions.append('ancestry')
+
+
+class Typed(Core):
+    """
+    A I{typed} marshaller.
+    This marshaller is semi-typed as needed to support both I{document/literal}
+    and I{rpc/literal} soap message styles.
+    @ivar schema: An xsd schema.
+    @type schema: L{xsd.schema.Schema}
+    @ivar resolver: A schema type resolver.
+    @type resolver: L{GraphResolver}
+    """
+
+    def __init__(self, schema, xstq=True):
+        """
+        @param schema: A schema object
+        @type schema: L{xsd.schema.Schema}
+        @param xstq: The B{x}ml B{s}chema B{t}ype B{q}ualified flag indicates
+            that the I{xsi:type} attribute values should be qualified by
+            namespace.
+        @type xstq: bool
+        """
+        Core.__init__(self)
+        self.schema = schema
+        self.xstq = xstq
+        self.resolver = GraphResolver(self.schema)
+
+    def reset(self):
+        self.resolver.reset()
+
+    def start(self, content):
+        #
+        # Start marshalling the 'content' by ensuring that both the 'content'
+        # _and_ the resolver are primed with the XSD type information. The
+        # 'content' value is both translated and sorted based on the XSD type.
+        # Only values that are objects have their attributes sorted.
+        #
+        log.debug('starting content:\n%s', content)
+        if content.type is None:
+            name = content.tag
+            if name.startswith('_'):
+                name = '@' + name[1:]
+            content.type = self.resolver.find(name, content.value)
+            if content.type is None:
+                raise TypeNotFound(content.tag)
+        else:
+            known = None
+            if isinstance(content.value, Object):
+                known = self.resolver.known(content.value)
+                if known is None:
+                    log.debug('object %s has no type information',
+                        content.value)
+                    known = content.type
+            frame = Frame(content.type, resolved=known)
+            self.resolver.push(frame)
+        frame = self.resolver.top()
+        content.real = frame.resolved
+        content.ancestry = frame.ancestry
+        self.translate(content)
+        self.sort(content)
+        if self.skip(content):
+            log.debug('skipping (optional) content:\n%s', content)
+            self.resolver.pop()
+            return False
+        return True
+
+    def suspend(self, content):
+        #
+        # Suspend to process a list content. Primarily, this involves popping
+        # the 'list' content off the resolver's stack its list items can be
+        # marshalled.
+        #
+        self.resolver.pop()
+
+    def resume(self, content):
+        #
+        # Resume processing a list content. To do this, we really need to
+        # simply push the 'list' content back onto the resolver stack.
+        #
+        self.resolver.push(Frame(content.type))
+
+    def end(self, parent, content):
+        #
+        # End processing the content. Make sure the content ending matches the
+        # top of the resolver stack since for list processing we play games
+        # with the resolver stack.
+        #
+        log.debug('ending content:\n%s', content)
+        current = self.resolver.top().type
+        if current == content.type:
+            self.resolver.pop()
+        else:
+            raise Exception, \
+                'content (end) mismatch: top=(%s) cont=(%s)' % \
+                (current, content)
+
+    def node(self, content):
+        #
+        # Create an XML node and namespace qualify as defined by the schema
+        # (elementFormDefault).
+        #
+        ns = content.type.namespace()
+        if content.type.form_qualified:
+            node = Element(content.tag, ns=ns)
+            if ns[0]:
+                node.addPrefix(ns[0], ns[1])
+        else:
+            node = Element(content.tag)
+        self.encode(node, content)
+        log.debug('created - node:\n%s', node)
+        return node
+
+    def setnil(self, node, content):
+        #
+        # Set the 'node' nil only if the XSD type specifies that it is
+        # permitted.
+        #
+        if content.type.nillable:
+            node.setnil()
+
+    def setdefault(self, node, content):
+        #
+        # Set the node to the default value specified by the XSD type.
+        #
+        default = content.type.default
+        if default is not None:
+            node.setText(default)
+        return default
+
+    def optional(self, content):
+        if content.type.optional():
+            return True
+        for a in content.ancestry:
+            if a.optional():
+                return True
+        return False
+
+    def encode(self, node, content):
+        # Add (soap) encoding information only if the resolved type is derived
+        # by extension. Further, the xsi:type values is qualified by namespace
+        # only if the content (tag) and referenced type are in different
+        # namespaces.
+        if content.type.any():
+            return
+        if not content.real.extension():
+            return
+        if content.type.resolve() == content.real:
+            return
+        ns = None
+        name = content.real.name
+        if self.xstq:
+            ns = content.real.namespace('ns1')
+        Typer.manual(node, name, ns)
+
+    def skip(self, content):
+        """
+        Get whether to skip this I{content}.
+        Should be skipped when the content is optional and either value=None or
+        the value is an empty list.
+        @param content: The content to skip.
+        @type content: L{Object}
+        @return: True if content is to be skipped.
+        @rtype: bool
+        """
+        if self.optional(content):
+            v = content.value
+            if v is None:
+                return True
+            if isinstance(v, (list, tuple)) and len(v) == 0:
+                return True
+        return False
+
+    def optional(self, content):
+        if content.type.optional():
+            return True
+        for a in content.ancestry:
+            if a.optional():
+                return True
+        return False
+
+    def translate(self, content):
+        """
+        Translate using the XSD type information.
+        Python I{dict} is translated to a suds object. Most importantly,
+        primative values are translated from python types to XML types using
+        the XSD type.
+        @param content: The content to translate.
+        @type content: L{Object}
+        @return: self
+        @rtype: L{Typed}
+        """
+        v = content.value
+        if v is None:
+            return
+        if isinstance(v, dict):
+            cls = content.real.name
+            content.value = Factory.object(cls, v)
+            md = content.value.__metadata__
+            md.sxtype = content.type
+            return
+        v = content.real.translate(v, False)
+        content.value = v
+        return self
+
+    def sort(self, content):
+        """
+        Sort suds object attributes based on ordering defined in the XSD type
+        information.
+        @param content: The content to sort.
+        @type content: L{Object}
+        @return: self
+        @rtype: L{Typed}
+        """
+        v = content.value
+        if isinstance(v, Object):
+            md = v.__metadata__
+            md.ordering = self.ordering(content.real)
+        return self
+
+    def ordering(self, type):
+        """
+        Get the attribute ordering defined in the specified XSD type
+        information.
+        @param type: An XSD type object.
+        @type type: SchemaObject
+        @return: An ordered list of attribute names.
+        @rtype: list
+        """
+        result = []
+        for child, ancestry in type.resolve():
+            name = child.name
+            if child.name is None:
+                continue
+            if child.isattr():
+                name = '_%s' % child.name
+            result.append(name)
+        return result
+
+
+class Literal(Typed):
+    """
+    A I{literal} marshaller.
+    This marshaller is semi-typed as needed to support both I{document/literal}
+    and I{rpc/literal} soap message styles.
+    """
+    pass
diff --git a/suds/mx/typer.py b/suds/mx/typer.py
new file mode 100644
index 00000000..dc16fa53
--- /dev/null
+++ b/suds/mx/typer.py
@@ -0,0 +1,119 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides sx typing classes.
+"""
+
+from suds import *
+from suds.mx import *
+from suds.sax import Namespace as NS
+from suds.sax.text import Text
+
+
+class Typer:
+    """
+    Provides XML node typing as either automatic or manual.
+    @cvar types:  A dict of class to xs type mapping.
+    @type types: dict
+    """
+
+    types = {
+        int : ('int', NS.xsdns),
+        long : ('long', NS.xsdns),
+        float : ('float', NS.xsdns),
+        str : ('string', NS.xsdns),
+        unicode : ('string', NS.xsdns),
+        Text : ('string', NS.xsdns),
+        bool : ('boolean', NS.xsdns),
+     }
+
+    @classmethod
+    def auto(cls, node, value=None):
+        """
+        Automatically set the node's xsi:type attribute based on either I{value}'s
+        class or the class of the node's text.  When I{value} is an unmapped class,
+        the default type (xs:any) is set.
+        @param node: An XML node
+        @type node: L{sax.element.Element}
+        @param value: An object that is or would be the node's text.
+        @type value: I{any}
+        @return: The specified node.
+        @rtype: L{sax.element.Element}
+        """
+        if value is None:
+            value = node.getText()
+        if isinstance(value, Object):
+            known = cls.known(value)
+            if known.name is None:
+                return node
+            tm = (known.name, known.namespace())
+        else:
+            tm = cls.types.get(value.__class__, cls.types.get(str))
+        cls.manual(node, *tm)
+        return node
+
+    @classmethod
+    def manual(cls, node, tval, ns=None):
+        """
+        Set the node's xsi:type attribute based on either I{value}'s
+        class or the class of the node's text.  Then adds the referenced
+        prefix(s) to the node's prefix mapping.
+        @param node: An XML node
+        @type node: L{sax.element.Element}
+        @param tval: The name of the schema type.
+        @type tval: str
+        @param ns: The XML namespace of I{tval}.
+        @type ns: (prefix, uri)
+        @return: The specified node.
+        @rtype: L{sax.element.Element}
+        """
+        xta = ':'.join((NS.xsins[0], 'type'))
+        node.addPrefix(NS.xsins[0], NS.xsins[1])
+        if ns is None:
+            node.set(xta, tval)
+        else:
+            ns = cls.genprefix(node, ns)
+            qname = ':'.join((ns[0], tval))
+            node.set(xta, qname)
+            node.addPrefix(ns[0], ns[1])
+        return node
+
+    @classmethod
+    def genprefix(cls, node, ns):
+        """
+        Generate a prefix.
+        @param node: An XML node on which the prefix will be used.
+        @type node: L{sax.element.Element}
+        @param ns: A namespace needing an unique prefix.
+        @type ns: (prefix, uri)
+        @return: The I{ns} with a new prefix.
+        """
+        for n in range(1, 1024):
+            p = 'ns%d' % n
+            u = node.resolvePrefix(p, default=None)
+            if u is None or u == ns[1]:
+                return (p, ns[1])
+        raise Exception('auto prefix, exhausted')
+
+    @classmethod
+    def known(cls, object):
+        try:
+            md = object.__metadata__
+            known = md.sxtype
+            return known
+        except:
+            pass
diff --git a/suds/options.py b/suds/options.py
new file mode 100644
index 00000000..2b2d4896
--- /dev/null
+++ b/suds/options.py
@@ -0,0 +1,150 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Suds basic options classes.
+"""
+
+from suds.cache import Cache, NoCache
+from suds.properties import *
+from suds.store import DocumentStore, defaultDocumentStore
+from suds.transport import Transport
+from suds.wsse import Security
+from suds.xsd.doctor import Doctor
+
+
+class TpLinker(AutoLinker):
+    """
+    Transport (auto) linker used to manage linkage between
+    transport objects Properties and those Properties that contain them.
+    """
+
+    def updated(self, properties, prev, next):
+        if isinstance(prev, Transport):
+            tp = Unskin(prev.options)
+            properties.unlink(tp)
+        if isinstance(next, Transport):
+            tp = Unskin(next.options)
+            properties.link(tp)
+
+
+class Options(Skin):
+    """
+    Options:
+        - B{cache} - The XML document cache. May be set to None for no caching.
+                - type: L{Cache}
+                - default: L{NoCache()}
+        - B{documentStore} - The XML document store used to access locally
+            stored documents without having to download them from an external
+            location. May be set to None for no internal suds library document
+            store.
+                - type: L{DocumentStore}
+                - default: L{defaultDocumentStore}
+        - B{extraArgumentErrors} - Raise exceptions when extra arguments are
+            detected when invoking a web service operation, compared to the
+            operation's WSDL schema definition.
+                - type: I{bool}
+                - default: True
+        - B{faults} - Raise faults raised by server, else return tuple from
+            service method invocation as (httpcode, object).
+                - type: I{bool}
+                - default: True
+        - B{service} - The default service name.
+                - type: I{str}
+                - default: None
+        - B{port} - The default service port name, not tcp port.
+                - type: I{str}
+                - default: None
+        - B{location} - This overrides the service port address I{URL} defined
+            in the WSDL.
+                - type: I{str}
+                - default: None
+        - B{transport} - The message transport.
+                - type: L{Transport}
+                - default: None
+        - B{soapheaders} - The soap headers to be included in the soap message.
+                - type: I{any}
+                - default: None
+        - B{wsse} - The web services I{security} provider object.
+                - type: L{Security}
+                - default: None
+        - B{doctor} - A schema I{doctor} object.
+                - type: L{Doctor}
+                - default: None
+        - B{xstq} - The B{x}ml B{s}chema B{t}ype B{q}ualified flag indicates
+            that the I{xsi:type} attribute values should be qualified by
+            namespace.
+                - type: I{bool}
+                - default: True
+        - B{prefixes} - Elements of the soap message should be qualified (when
+            needed) using XML prefixes as opposed to xmlns="" syntax.
+                - type: I{bool}
+                - default: True
+        - B{retxml} - Flag that causes the I{raw} soap envelope to be returned
+            instead of the python object graph.
+                - type: I{bool}
+                - default: False
+        - B{prettyxml} - Flag that causes I{pretty} xml to be rendered when
+            generating the outbound soap envelope.
+                - type: I{bool}
+                - default: False
+        - B{autoblend} - Flag that ensures that the schema(s) defined within
+            the WSDL import each other.
+                - type: I{bool}
+                - default: False
+        - B{cachingpolicy} - The caching policy.
+                - type: I{int}
+                  - 0 = Cache XML documents.
+                  - 1 = Cache WSDL (pickled) object.
+                - default: 0
+        - B{plugins} - A plugin container.
+                - type: I{list}
+                - default: I{list()}
+        - B{nosend} - Create the soap envelope but do not send.
+            When specified, method invocation returns a I{RequestContext}
+            instead of sending it.
+                - type: I{bool}
+                - default: False
+        - B{unwrap} - Enable automatic parameter unwrapping when possible.
+            Enabled by default. If disabled, no input or output parameters are
+            ever automatically unwrapped.
+                - type: I{bool}
+                - default: True
+    """
+    def __init__(self, **kwargs):
+        domain = __name__
+        definitions = [
+            Definition('cache', Cache, NoCache()),
+            Definition('documentStore', DocumentStore, defaultDocumentStore),
+            Definition('extraArgumentErrors', bool, True),
+            Definition('faults', bool, True),
+            Definition('transport', Transport, None, TpLinker()),
+            Definition('service', (int, basestring), None),
+            Definition('port', (int, basestring), None),
+            Definition('location', basestring, None),
+            Definition('soapheaders', (), ()),
+            Definition('wsse', Security, None),
+            Definition('doctor', Doctor, None),
+            Definition('xstq', bool, True),
+            Definition('prefixes', bool, True),
+            Definition('retxml', bool, False),
+            Definition('prettyxml', bool, False),
+            Definition('autoblend', bool, False),
+            Definition('cachingpolicy', int, 0),
+            Definition('plugins', (list, tuple), []),
+            Definition('nosend', bool, False),
+            Definition('unwrap', bool, True)]
+        Skin.__init__(self, domain, definitions, kwargs)
diff --git a/suds/plugin.py b/suds/plugin.py
new file mode 100644
index 00000000..3579f5c4
--- /dev/null
+++ b/suds/plugin.py
@@ -0,0 +1,257 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The plugin module provides classes for implementation
+of suds plugins.
+"""
+
+from suds import *
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Context(object):
+    """
+    Plugin context.
+    """
+    pass
+
+
+class InitContext(Context):
+    """
+    Init Context.
+    @ivar wsdl: The wsdl.
+    @type wsdl: L{wsdl.Definitions}
+    """
+    pass
+
+
+class DocumentContext(Context):
+    """
+    The XML document load context.
+    @ivar url: The URL.
+    @type url: str
+    @ivar document: Either the XML text or the B{parsed} document root.
+    @type document: (str|L{sax.element.Element})
+    """
+    pass
+
+
+class MessageContext(Context):
+    """
+    The context for sending the SOAP envelope.
+    @ivar envelope: The SOAP envelope to be sent.
+    @type envelope: (str|L{sax.element.Element})
+    @ivar reply: The reply.
+    @type reply: (str|L{sax.element.Element}|object)
+    """
+    pass
+
+
+class Plugin:
+    """
+    Plugin base.
+    """
+    pass
+
+
+class InitPlugin(Plugin):
+    """
+    The base class for suds I{init} plugins.
+    """
+
+    def initialized(self, context):
+        """
+        Suds client initialization.
+        Called after wsdl the has been loaded.  Provides the plugin
+        with the opportunity to inspect/modify the WSDL.
+        @param context: The init context.
+        @type context: L{InitContext}
+        """
+        pass
+
+
+class DocumentPlugin(Plugin):
+    """
+    The base class for suds I{document} plugins.
+    """
+
+    def loaded(self, context):
+        """
+        Suds has loaded a WSDL/XSD document.  Provides the plugin
+        with an opportunity to inspect/modify the unparsed document.
+        Called after each WSDL/XSD document is loaded.
+        @param context: The document context.
+        @type context: L{DocumentContext}
+        """
+        pass
+
+    def parsed(self, context):
+        """
+        Suds has parsed a WSDL/XSD document.  Provides the plugin
+        with an opportunity to inspect/modify the parsed document.
+        Called after each WSDL/XSD document is parsed.
+        @param context: The document context.
+        @type context: L{DocumentContext}
+        """
+        pass
+
+
+class MessagePlugin(Plugin):
+    """
+    The base class for suds I{SOAP message} plugins.
+    """
+
+    def marshalled(self, context):
+        """
+        Suds will send the specified soap envelope.
+        Provides the plugin with the opportunity to inspect/modify
+        the envelope Document before it is sent.
+        @param context: The send context.
+            The I{envelope} is the envelope document.
+        @type context: L{MessageContext}
+        """
+        pass
+
+    def sending(self, context):
+        """
+        Suds will send the specified SOAP envelope.
+        Provides the plugin with the opportunity to inspect/modify
+        the message text it is sent.
+        @param context: The send context.
+            The I{envelope} is the envelope text.
+        @type context: L{MessageContext}
+        """
+        pass
+
+    def received(self, context):
+        """
+        Suds has received the specified reply.
+        Provides the plugin with the opportunity to inspect/modify
+        the received XML text before it is SAX parsed.
+        @param context: The reply context.
+            The I{reply} is the raw text.
+        @type context: L{MessageContext}
+        """
+        pass
+
+    def parsed(self, context):
+        """
+        Suds has sax parsed the received reply.
+        Provides the plugin with the opportunity to inspect/modify
+        the sax parsed DOM tree for the reply before it is unmarshalled.
+        @param context: The reply context.
+            The I{reply} is DOM tree.
+        @type context: L{MessageContext}
+        """
+        pass
+
+    def unmarshalled(self, context):
+        """
+        Suds has unmarshalled the received reply.
+        Provides the plugin with the opportunity to inspect/modify
+        the unmarshalled reply object before it is returned.
+        @param context: The reply context.
+            The I{reply} is unmarshalled suds object.
+        @type context: L{MessageContext}
+        """
+        pass
+
+
+class PluginContainer:
+    """
+    Plugin container provides easy method invocation.
+    @ivar plugins: A list of plugin objects.
+    @type plugins: [L{Plugin},]
+    @cvar ctxclass: A dict of plugin method / context classes.
+    @type ctxclass: dict
+    """
+
+    domains = {\
+        'init': (InitContext, InitPlugin),
+        'document': (DocumentContext, DocumentPlugin),
+        'message': (MessageContext, MessagePlugin ),
+    }
+
+    def __init__(self, plugins):
+        """
+        @param plugins: A list of plugin objects.
+        @type plugins: [L{Plugin},]
+        """
+        self.plugins = plugins
+
+    def __getattr__(self, name):
+        domain = self.domains.get(name)
+        if domain:
+            plugins = []
+            ctx, pclass = domain
+            for p in self.plugins:
+                if isinstance(p, pclass):
+                    plugins.append(p)
+            return PluginDomain(ctx, plugins)
+        else:
+            raise Exception, 'plugin domain (%s), invalid' % name
+
+
+class PluginDomain:
+    """
+    The plugin domain.
+    @ivar ctx: A context.
+    @type ctx: L{Context}
+    @ivar plugins: A list of plugins (targets).
+    @type plugins: list
+    """
+
+    def __init__(self, ctx, plugins):
+        self.ctx = ctx
+        self.plugins = plugins
+
+    def __getattr__(self, name):
+        return Method(name, self)
+
+
+class Method:
+    """
+    Plugin method.
+    @ivar name: The method name.
+    @type name: str
+    @ivar domain: The plugin domain.
+    @type domain: L{PluginDomain}
+    """
+
+    def __init__(self, name, domain):
+        """
+        @param name: The method name.
+        @type name: str
+        @param domain: A plugin domain.
+        @type domain: L{PluginDomain}
+        """
+        self.name = name
+        self.domain = domain
+
+    def __call__(self, **kwargs):
+        ctx = self.domain.ctx()
+        ctx.__dict__.update(kwargs)
+        for plugin in self.domain.plugins:
+            try:
+                method = getattr(plugin, self.name, None)
+                if method and callable(method):
+                    method(ctx)
+            except Exception, pe:
+                log.exception(pe)
+        return ctx
diff --git a/suds/properties.py b/suds/properties.py
new file mode 100644
index 00000000..5907d943
--- /dev/null
+++ b/suds/properties.py
@@ -0,0 +1,539 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Properties classes.
+"""
+
+
+class AutoLinker(object):
+    """
+    Base class, provides interface for I{automatic} link
+    management between a L{Properties} object and the L{Properties}
+    contained within I{values}.
+    """
+    def updated(self, properties, prev, next):
+        """
+        Notification that a values was updated and the linkage
+        between the I{properties} contained with I{prev} need to
+        be relinked to the L{Properties} contained within the
+        I{next} value.
+        """
+        pass
+
+
+class Link(object):
+    """
+    Property link object.
+    @ivar endpoints: A tuple of the (2) endpoints of the link.
+    @type endpoints: tuple(2)
+    """
+    def __init__(self, a, b):
+        """
+        @param a: Property (A) to link.
+        @type a: L{Property}
+        @param b: Property (B) to link.
+        @type b: L{Property}
+        """
+        pA = Endpoint(self, a)
+        pB = Endpoint(self, b)
+        self.endpoints = (pA, pB)
+        self.validate(a, b)
+        a.links.append(pB)
+        b.links.append(pA)
+
+    def validate(self, pA, pB):
+        """
+        Validate that the two properties may be linked.
+        @param pA: Endpoint (A) to link.
+        @type pA: L{Endpoint}
+        @param pB: Endpoint (B) to link.
+        @type pB: L{Endpoint}
+        @return: self
+        @rtype: L{Link}
+        """
+        if pA in pB.links or \
+           pB in pA.links:
+            raise Exception, 'Already linked'
+        dA = pA.domains()
+        dB = pB.domains()
+        for d in dA:
+            if d in dB:
+                raise Exception, 'Duplicate domain "%s" found' % d
+        for d in dB:
+            if d in dA:
+                raise Exception, 'Duplicate domain "%s" found' % d
+        kA = pA.keys()
+        kB = pB.keys()
+        for k in kA:
+            if k in kB:
+                raise Exception, 'Duplicate key %s found' % k
+        for k in kB:
+            if k in kA:
+                raise Exception, 'Duplicate key %s found' % k
+        return self
+
+    def teardown(self):
+        """
+        Teardown the link.
+        Removes endpoints from properties I{links} collection.
+        @return: self
+        @rtype: L{Link}
+        """
+        pA, pB = self.endpoints
+        if pA in pB.links:
+            pB.links.remove(pA)
+        if pB in pA.links:
+            pA.links.remove(pB)
+        return self
+
+
+class Endpoint(object):
+    """
+    Link endpoint (wrapper).
+    @ivar link: The associated link.
+    @type link: L{Link}
+    @ivar target: The properties object.
+    @type target: L{Property}
+    """
+    def __init__(self, link, target):
+        self.link = link
+        self.target = target
+
+    def teardown(self):
+        return self.link.teardown()
+
+    def __eq__(self, rhs):
+        return ( self.target == rhs )
+
+    def __hash__(self):
+        return hash(self.target)
+
+    def __getattr__(self, name):
+        return getattr(self.target, name)
+
+
+class Definition:
+    """
+    Property definition.
+    @ivar name: The property name.
+    @type name: str
+    @ivar classes: The (class) list of permitted values
+    @type classes: tuple
+    @ivar default: The default value.
+    @ivar type: any
+    """
+    def __init__(self, name, classes, default, linker=AutoLinker()):
+        """
+        @param name: The property name.
+        @type name: str
+        @param classes: The (class) list of permitted values
+        @type classes: tuple
+        @param default: The default value.
+        @type default: any
+        """
+        if not isinstance(classes, (list, tuple)):
+            classes = (classes,)
+        self.name = name
+        self.classes = classes
+        self.default = default
+        self.linker = linker
+
+    def nvl(self, value=None):
+        """
+        Convert the I{value} into the default when I{None}.
+        @param value: The proposed value.
+        @type value: any
+        @return: The I{default} when I{value} is I{None}, else I{value}.
+        @rtype: any
+        """
+        if value is None:
+            return self.default
+        else:
+            return value
+
+    def validate(self, value):
+        """
+        Validate the I{value} is of the correct class.
+        @param value: The value to validate.
+        @type value: any
+        @raise AttributeError: When I{value} is invalid.
+        """
+        if value is None:
+            return
+        if len(self.classes) and \
+            not isinstance(value, self.classes):
+                msg = '"%s" must be: %s' % (self.name, self.classes)
+                raise AttributeError,msg
+
+
+    def __repr__(self):
+        return '%s: %s' % (self.name, str(self))
+
+    def __str__(self):
+        s = []
+        if len(self.classes):
+            s.append('classes=%s' % str(self.classes))
+        else:
+            s.append('classes=*')
+        s.append("default=%s" % str(self.default))
+        return ', '.join(s)
+
+
+class Properties:
+    """
+    Represents basic application properties.
+    Provides basic type validation, default values and
+    link/synchronization behavior.
+    @ivar domain: The domain name.
+    @type domain: str
+    @ivar definitions: A table of property definitions.
+    @type definitions: {name: L{Definition}}
+    @ivar links: A list of linked property objects used to create
+        a network of properties.
+    @type links: [L{Property},..]
+    @ivar defined: A dict of property values.
+    @type defined: dict
+    """
+    def __init__(self, domain, definitions, kwargs):
+        """
+        @param domain: The property domain name.
+        @type domain: str
+        @param definitions: A table of property definitions.
+        @type definitions: {name: L{Definition}}
+        @param kwargs: A list of property name/values to set.
+        @type kwargs: dict
+        """
+        self.definitions = {}
+        for d in definitions:
+            self.definitions[d.name] = d
+        self.domain = domain
+        self.links = []
+        self.defined = {}
+        self.modified = set()
+        self.prime()
+        self.update(kwargs)
+
+    def definition(self, name):
+        """
+        Get the definition for the property I{name}.
+        @param name: The property I{name} to find the definition for.
+        @type name: str
+        @return: The property definition
+        @rtype: L{Definition}
+        @raise AttributeError: On not found.
+        """
+        d = self.definitions.get(name)
+        if d is None:
+            raise AttributeError(name)
+        return d
+
+    def update(self, other):
+        """
+        Update the property values as specified by keyword/value.
+        @param other: An object to update from.
+        @type other: (dict|L{Properties})
+        @return: self
+        @rtype: L{Properties}
+        """
+        if isinstance(other, Properties):
+            other = other.defined
+        for n,v in other.items():
+            self.set(n, v)
+        return self
+
+    def notset(self, name):
+        """
+        Get whether a property has never been set by I{name}.
+        @param name: A property name.
+        @type name: str
+        @return: True if never been set.
+        @rtype: bool
+        """
+        self.provider(name).__notset(name)
+
+    def set(self, name, value):
+        """
+        Set the I{value} of a property by I{name}.
+        The value is validated against the definition and set
+        to the default when I{value} is None.
+        @param name: The property name.
+        @type name: str
+        @param value: The new property value.
+        @type value: any
+        @return: self
+        @rtype: L{Properties}
+        """
+        self.provider(name).__set(name, value)
+        return self
+
+    def unset(self, name):
+        """
+        Unset a property by I{name}.
+        @param name: A property name.
+        @type name: str
+        @return: self
+        @rtype: L{Properties}
+        """
+        self.provider(name).__set(name, None)
+        return self
+
+    def get(self, name, *df):
+        """
+        Get the value of a property by I{name}.
+        @param name: The property name.
+        @type name: str
+        @param df: An optional value to be returned when the value
+            is not set
+        @type df: [1].
+        @return: The stored value, or I{df[0]} if not set.
+        @rtype: any
+        """
+        return self.provider(name).__get(name, *df)
+
+    def link(self, other):
+        """
+        Link (associate) this object with anI{other} properties object
+        to create a network of properties.  Links are bidirectional.
+        @param other: The object to link.
+        @type other: L{Properties}
+        @return: self
+        @rtype: L{Properties}
+        """
+        Link(self, other)
+        return self
+
+    def unlink(self, *others):
+        """
+        Unlink (disassociate) the specified properties object.
+        @param others: The list object to unlink.  Unspecified means unlink all.
+        @type others: [L{Properties},..]
+        @return: self
+        @rtype: L{Properties}
+        """
+        if not len(others):
+            others = self.links[:]
+        for p in self.links[:]:
+            if p in others:
+                p.teardown()
+        return self
+
+    def provider(self, name, history=None):
+        """
+        Find the provider of the property by I{name}.
+        @param name: The property name.
+        @type name: str
+        @param history: A history of nodes checked to prevent
+            circular hunting.
+        @type history: [L{Properties},..]
+        @return: The provider when found.  Otherwise, None (when nested)
+            and I{self} when not nested.
+        @rtype: L{Properties}
+        """
+        if history is None:
+            history = []
+        history.append(self)
+        if name in self.definitions:
+            return self
+        for x in self.links:
+            if x in history:
+                continue
+            provider = x.provider(name, history)
+            if provider is not None:
+                return provider
+        history.remove(self)
+        if len(history):
+            return None
+        return self
+
+    def keys(self, history=None):
+        """
+        Get the set of I{all} property names.
+        @param history: A history of nodes checked to prevent
+            circular hunting.
+        @type history: [L{Properties},..]
+        @return: A set of property names.
+        @rtype: list
+        """
+        if history is None:
+            history = []
+        history.append(self)
+        keys = set()
+        keys.update(self.definitions.keys())
+        for x in self.links:
+            if x in history:
+                continue
+            keys.update(x.keys(history))
+        history.remove(self)
+        return keys
+
+    def domains(self, history=None):
+        """
+        Get the set of I{all} domain names.
+        @param history: A history of nodes checked to prevent
+            circular hunting.
+        @type history: [L{Properties},..]
+        @return: A set of domain names.
+        @rtype: list
+        """
+        if history is None:
+            history = []
+        history.append(self)
+        domains = set()
+        domains.add(self.domain)
+        for x in self.links:
+            if x in history:
+                continue
+            domains.update(x.domains(history))
+        history.remove(self)
+        return domains
+
+    def prime(self):
+        """
+        Prime the stored values based on default values
+        found in property definitions.
+        @return: self
+        @rtype: L{Properties}
+        """
+        for d in self.definitions.values():
+            self.defined[d.name] = d.default
+        return self
+
+    def __notset(self, name):
+        return not (name in self.modified)
+
+    def __set(self, name, value):
+        d = self.definition(name)
+        d.validate(value)
+        value = d.nvl(value)
+        prev = self.defined[name]
+        self.defined[name] = value
+        self.modified.add(name)
+        d.linker.updated(self, prev, value)
+
+    def __get(self, name, *df):
+        d = self.definition(name)
+        value = self.defined.get(name)
+        if value == d.default and len(df):
+            value = df[0]
+        return value
+
+    def str(self, history):
+        s = []
+        s.append('Definitions:')
+        for d in self.definitions.values():
+            s.append('\t%s' % repr(d))
+        s.append('Content:')
+        for d in self.defined.items():
+            s.append('\t%s' % str(d))
+        if self not in history:
+            history.append(self)
+            s.append('Linked:')
+            for x in self.links:
+                s.append(x.str(history))
+            history.remove(self)
+        return '\n'.join(s)
+
+    def __repr__(self):
+        return str(self)
+
+    def __str__(self):
+        return self.str([])
+
+
+class Skin(object):
+    """
+    The meta-programming I{skin} around the L{Properties} object.
+    @ivar __pts__: The wrapped object.
+    @type __pts__: L{Properties}.
+    """
+    def __init__(self, domain, definitions, kwargs):
+        self.__pts__ = Properties(domain, definitions, kwargs)
+
+    def __setattr__(self, name, value):
+        builtin = name.startswith('__') and name.endswith('__')
+        if builtin:
+            self.__dict__[name] = value
+            return
+        self.__pts__.set(name, value)
+
+    def __getattr__(self, name):
+        return self.__pts__.get(name)
+
+    def __repr__(self):
+        return str(self)
+
+    def __str__(self):
+        return str(self.__pts__)
+
+
+class Unskin(object):
+    def __new__(self, *args, **kwargs):
+        return args[0].__pts__
+
+
+class Inspector:
+    """
+    Wrapper inspector.
+    """
+    def __init__(self, options):
+        self.properties = options.__pts__
+
+    def get(self, name, *df):
+        """
+        Get the value of a property by I{name}.
+        @param name: The property name.
+        @type name: str
+        @param df: An optional value to be returned when the value
+            is not set
+        @type df: [1].
+        @return: The stored value, or I{df[0]} if not set.
+        @rtype: any
+        """
+        return self.properties.get(name, *df)
+
+    def update(self, **kwargs):
+        """
+        Update the property values as specified by keyword/value.
+        @param kwargs: A list of property name/values to set.
+        @type kwargs: dict
+        @return: self
+        @rtype: L{Properties}
+        """
+        return self.properties.update(**kwargs)
+
+    def link(self, other):
+        """
+        Link (associate) this object with anI{other} properties object
+        to create a network of properties.  Links are bidirectional.
+        @param other: The object to link.
+        @type other: L{Properties}
+        @return: self
+        @rtype: L{Properties}
+        """
+        p = other.__pts__
+        return self.properties.link(p)
+
+    def unlink(self, other):
+        """
+        Unlink (disassociate) the specified properties object.
+        @param other: The object to unlink.
+        @type other: L{Properties}
+        @return: self
+        @rtype: L{Properties}
+        """
+        p = other.__pts__
+        return self.properties.unlink(p)
diff --git a/suds/reader.py b/suds/reader.py
new file mode 100644
index 00000000..ad71642a
--- /dev/null
+++ b/suds/reader.py
@@ -0,0 +1,166 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+  XML document reader classes providing integration with the suds library's
+caching system.
+"""
+
+
+from suds.cache import Cache, NoCache
+from suds.plugin import PluginContainer
+from suds.sax.parser import Parser
+from suds.store import DocumentStore
+from suds.transport import Request
+
+
+class Reader:
+    """
+    Provides integration with the cache.
+    @ivar options: An options object.
+    @type options: I{Options}
+    """
+
+    def __init__(self, options):
+        """
+        @param options: An options object.
+        @type options: I{Options}
+        """
+        self.options = options
+        self.plugins = PluginContainer(options.plugins)
+
+    def mangle(self, name, x):
+        """
+        Mangle the name by hashing the I{name} and appending I{x}.
+        @return: the mangled name.
+        """
+        h = abs(hash(name))
+        return '%s-%s' % (h, x)
+
+
+class DocumentReader(Reader):
+    """
+    Provides integration between the SAX L{Parser} and the document cache.
+    """
+
+    def open(self, url):
+        """
+        Open an XML document at the specified I{URL}.
+        First, the document attempted to be retrieved from the I{object cache}.
+        If not found, it is downloaded and parsed using the SAX parser. The
+        result is added to the cache for the next open().
+        @param url: A document URL.
+        @type url: str.
+        @return: The specified XML document.
+        @rtype: I{Document}
+        """
+        cache = self.cache()
+        id = self.mangle(url, 'document')
+        d = cache.get(id)
+        if d is None:
+            d = self.download(url)
+            cache.put(id, d)
+        self.plugins.document.parsed(url=url, document=d.root())
+        return d
+
+    def download(self, url):
+        """
+        Download the document.
+        @param url: A document URL.
+        @type url: str.
+        @return: A file pointer to the document.
+        @rtype: file-like
+        """
+        content = None
+        store = self.options.documentStore
+        if store is not None:
+            content = store.open(url)
+        if content is None:
+            fp = self.options.transport.open(Request(url))
+            try:
+                content = fp.read()
+            finally:
+                fp.close()
+        ctx = self.plugins.document.loaded(url=url, document=content)
+        content = ctx.document
+        sax = Parser()
+        return sax.parse(string=content)
+
+    def cache(self):
+        """
+        Get the cache.
+        @return: The I{cache} when I{cachingpolicy} = B{0}.
+        @rtype: L{Cache}
+        """
+        if self.options.cachingpolicy == 0:
+            return self.options.cache
+        return NoCache()
+
+
+class DefinitionsReader(Reader):
+    """
+    Provides integration between the WSDL Definitions object and the object
+    cache.
+    @ivar fn: A factory function (constructor) used to
+        create the object not found in the cache.
+    @type fn: I{Constructor}
+    """
+
+    def __init__(self, options, fn):
+        """
+        @param options: An options object.
+        @type options: I{Options}
+        @param fn: A factory function (constructor) used to create the object
+            not found in the cache.
+        @type fn: I{Constructor}
+        """
+        Reader.__init__(self, options)
+        self.fn = fn
+
+    def open(self, url):
+        """
+        Open a WSDL at the specified I{URL}.
+        First, the WSDL attempted to be retrieved from
+        the I{object cache}.  After unpickled from the cache, the
+        I{options} attribute is restored.
+        If not found, it is downloaded and instantiated using the
+        I{fn} constructor and added to the cache for the next open().
+        @param url: A WSDL URL.
+        @type url: str.
+        @return: The WSDL object.
+        @rtype: I{Definitions}
+        """
+        cache = self.cache()
+        id = self.mangle(url, 'wsdl')
+        d = cache.get(id)
+        if d is None:
+            d = self.fn(url, self.options)
+            cache.put(id, d)
+        else:
+            d.options = self.options
+            for imp in d.imports:
+                imp.imported.options = self.options
+        return d
+
+    def cache(self):
+        """
+        Get the cache.
+        @return: The I{cache} when I{cachingpolicy} = B{1}.
+        @rtype: L{Cache}
+        """
+        if self.options.cachingpolicy == 1:
+            return self.options.cache
+        return NoCache()
diff --git a/suds/resolver.py b/suds/resolver.py
new file mode 100644
index 00000000..2a301696
--- /dev/null
+++ b/suds/resolver.py
@@ -0,0 +1,493 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{resolver} module provides a collection of classes that
+provide wsdl/xsd named type resolution.
+"""
+
+from suds import *
+from suds.sax import splitPrefix, Namespace
+from suds.sudsobject import Object
+from suds.xsd.query import BlindQuery, TypeQuery, qualify
+
+import re
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Resolver:
+    """
+    An I{abstract} schema-type resolver.
+    @ivar schema: A schema object.
+    @type schema: L{xsd.schema.Schema}
+    """
+
+    def __init__(self, schema):
+        """
+        @param schema: A schema object.
+        @type schema: L{xsd.schema.Schema}
+        """
+        self.schema = schema
+
+    def find(self, name, resolved=True):
+        """
+        Get the definition object for the schema object by name.
+        @param name: The name of a schema object.
+        @type name: basestring
+        @param resolved: A flag indicating that the fully resolved type
+            should be returned.
+        @type resolved: boolean
+        @return: The found schema I{type}
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        log.debug('searching schema for (%s)', name)
+        qref = qualify(name, self.schema.root, self.schema.tns)
+        query = BlindQuery(qref)
+        result = query.execute(self.schema)
+        if result is None:
+            log.error('(%s) not-found', name)
+            return None
+        log.debug('found (%s) as (%s)', name, Repr(result))
+        if resolved:
+            result = result.resolve()
+        return result
+
+
+class PathResolver(Resolver):
+    """
+    Resolves the definition object for the schema type located at a given path.
+    The path may contain (.) dot notation to specify nested types.
+    @ivar wsdl: A wsdl object.
+    @type wsdl: L{wsdl.Definitions}
+    """
+
+    def __init__(self, wsdl, ps='.'):
+        """
+        @param wsdl: A schema object.
+        @type wsdl: L{wsdl.Definitions}
+        @param ps: The path separator character
+        @type ps: char
+        """
+        Resolver.__init__(self, wsdl.schema)
+        self.wsdl = wsdl
+        self.altp = re.compile('({)(.+)(})(.+)')
+        self.splitp = re.compile('({.+})*[^\%s]+' % ps[0])
+
+    def find(self, path, resolved=True):
+        """
+        Get the definition object for the schema type located at the specified path.
+        The path may contain (.) dot notation to specify nested types.
+        Actually, the path separator is usually a (.) but can be redefined
+        during contruction.
+        @param path: A (.) separated path to a schema type.
+        @type path: basestring
+        @param resolved: A flag indicating that the fully resolved type
+            should be returned.
+        @type resolved: boolean
+        @return: The found schema I{type}
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        result = None
+        parts = self.split(path)
+        try:
+            result = self.root(parts)
+            if len(parts) > 1:
+                result = result.resolve(nobuiltin=True)
+                result = self.branch(result, parts)
+                result = self.leaf(result, parts)
+            if resolved:
+                result = result.resolve(nobuiltin=True)
+        except PathResolver.BadPath:
+            log.error('path: "%s", not-found' % path)
+        return result
+
+    def root(self, parts):
+        """
+        Find the path root.
+        @param parts: A list of path parts.
+        @type parts: [str,..]
+        @return: The root.
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        result = None
+        name = parts[0]
+        log.debug('searching schema for (%s)', name)
+        qref = self.qualify(parts[0])
+        query = BlindQuery(qref)
+        result = query.execute(self.schema)
+        if result is None:
+            log.error('(%s) not-found', name)
+            raise PathResolver.BadPath(name)
+        log.debug('found (%s) as (%s)', name, Repr(result))
+        return result
+
+    def branch(self, root, parts):
+        """
+        Traverse the path until a leaf is reached.
+        @param parts: A list of path parts.
+        @type parts: [str,..]
+        @param root: The root.
+        @type root: L{xsd.sxbase.SchemaObject}
+        @return: The end of the branch.
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        result = root
+        for part in parts[1:-1]:
+            name = splitPrefix(part)[1]
+            log.debug('searching parent (%s) for (%s)', Repr(result), name)
+            result, ancestry = result.get_child(name)
+            if result is None:
+                log.error('(%s) not-found', name)
+                raise PathResolver.BadPath(name)
+            result = result.resolve(nobuiltin=True)
+            log.debug('found (%s) as (%s)', name, Repr(result))
+        return result
+
+    def leaf(self, parent, parts):
+        """
+        Find the leaf.
+        @param parts: A list of path parts.
+        @type parts: [str,..]
+        @param parent: The leaf's parent.
+        @type parent: L{xsd.sxbase.SchemaObject}
+        @return: The leaf.
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        name = splitPrefix(parts[-1])[1]
+        if name.startswith('@'):
+            result, path = parent.get_attribute(name[1:])
+        else:
+            result, ancestry = parent.get_child(name)
+        if result is None:
+            raise PathResolver.BadPath(name)
+        return result
+
+    def qualify(self, name):
+        """
+        Qualify the name as either:
+          - plain name
+          - ns prefixed name (eg: ns0:Person)
+          - fully ns qualified name (eg: {http://myns-uri}Person)
+        @param name: The name of an object in the schema.
+        @type name: str
+        @return: A qualified name.
+        @rtype: qname
+        """
+        m = self.altp.match(name)
+        if m is None:
+            return qualify(name, self.wsdl.root, self.wsdl.tns)
+        else:
+            return (m.group(4), m.group(2))
+
+    def split(self, s):
+        """
+        Split the string on (.) while preserving any (.) inside the
+        '{}' alternalte syntax for full ns qualification.
+        @param s: A plain or qualified name.
+        @type s: str
+        @return: A list of the name's parts.
+        @rtype: [str,..]
+        """
+        parts = []
+        b = 0
+        while 1:
+            m = self.splitp.match(s, b)
+            if m is None:
+                break
+            b,e = m.span()
+            parts.append(s[b:e])
+            b = e+1
+        return parts
+
+    class BadPath(Exception): pass
+
+
+class TreeResolver(Resolver):
+    """
+    The tree resolver is a I{stateful} tree resolver
+    used to resolve each node in a tree.  As such, it mirrors
+    the tree structure to ensure that nodes are resolved in
+    context.
+    @ivar stack: The context stack.
+    @type stack: list
+    """
+
+    def __init__(self, schema):
+        """
+        @param schema: A schema object.
+        @type schema: L{xsd.schema.Schema}
+        """
+        Resolver.__init__(self, schema)
+        self.stack = Stack()
+
+    def reset(self):
+        """
+        Reset the resolver's state.
+        """
+        self.stack = Stack()
+
+    def push(self, x):
+        """
+        Push an I{object} onto the stack.
+        @param x: An object to push.
+        @type x: L{Frame}
+        @return: The pushed frame.
+        @rtype: L{Frame}
+        """
+        if isinstance(x, Frame):
+            frame = x
+        else:
+            frame = Frame(x)
+        self.stack.append(frame)
+        log.debug('push: (%s)\n%s', Repr(frame), Repr(self.stack))
+        return frame
+
+    def top(self):
+        """
+        Get the I{frame} at the top of the stack.
+        @return: The top I{frame}, else None.
+        @rtype: L{Frame}
+        """
+        if len(self.stack):
+            return self.stack[-1]
+        else:
+            return Frame.Empty()
+
+    def pop(self):
+        """
+        Pop the frame at the top of the stack.
+        @return: The popped frame, else None.
+        @rtype: L{Frame}
+        """
+        if len(self.stack):
+            popped = self.stack.pop()
+            log.debug('pop: (%s)\n%s', Repr(popped), Repr(self.stack))
+            return popped
+        log.debug('stack empty, not-popped')
+        return None
+
+    def depth(self):
+        """
+        Get the current stack depth.
+        @return: The current stack depth.
+        @rtype: int
+        """
+        return len(self.stack)
+
+    def getchild(self, name, parent):
+        """Get a child by name."""
+        log.debug('searching parent (%s) for (%s)', Repr(parent), name)
+        if name.startswith('@'):
+            return parent.get_attribute(name[1:])
+        return parent.get_child(name)
+
+
+class NodeResolver(TreeResolver):
+    """
+    The node resolver is a I{stateful} XML document resolver
+    used to resolve each node in a tree.  As such, it mirrors
+    the tree structure to ensure that nodes are resolved in
+    context.
+    """
+
+    def __init__(self, schema):
+        """
+        @param schema: A schema object.
+        @type schema: L{xsd.schema.Schema}
+        """
+        TreeResolver.__init__(self, schema)
+
+    def find(self, node, resolved=False, push=True):
+        """
+        @param node: An xml node to be resolved.
+        @type node: L{sax.element.Element}
+        @param resolved: A flag indicating that the fully resolved type should be
+            returned.
+        @type resolved: boolean
+        @param push: Indicates that the resolved type should be
+            pushed onto the stack.
+        @type push: boolean
+        @return: The found schema I{type}
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        name = node.name
+        parent = self.top().resolved
+        if parent is None:
+            result, ancestry = self.query(name, node)
+        else:
+            result, ancestry = self.getchild(name, parent)
+        known = self.known(node)
+        if result is None:
+            return result
+        if push:
+            frame = Frame(result, resolved=known, ancestry=ancestry)
+            pushed = self.push(frame)
+        if resolved:
+            result = result.resolve()
+        return result
+
+    def findattr(self, name, resolved=True):
+        """
+        Find an attribute type definition.
+        @param name: An attribute name.
+        @type name: basestring
+        @param resolved: A flag indicating that the fully resolved type should be
+            returned.
+        @type resolved: boolean
+        @return: The found schema I{type}
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        name = '@%s'%name
+        parent = self.top().resolved
+        if parent is None:
+            result, ancestry = self.query(name, node)
+        else:
+            result, ancestry = self.getchild(name, parent)
+        if result is None:
+            return result
+        if resolved:
+            result = result.resolve()
+        return result
+
+    def query(self, name, node):
+        """Blindly query the schema by name."""
+        log.debug('searching schema for (%s)', name)
+        qref = qualify(name, node, node.namespace())
+        query = BlindQuery(qref)
+        result = query.execute(self.schema)
+        return (result, [])
+
+    def known(self, node):
+        """Resolve type referenced by @xsi:type."""
+        ref = node.get('type', Namespace.xsins)
+        if ref is None:
+            return None
+        qref = qualify(ref, node, node.namespace())
+        query = BlindQuery(qref)
+        return query.execute(self.schema)
+
+
+class GraphResolver(TreeResolver):
+    """
+    The graph resolver is a I{stateful} L{Object} graph resolver
+    used to resolve each node in a tree.  As such, it mirrors
+    the tree structure to ensure that nodes are resolved in
+    context.
+    """
+
+    def __init__(self, schema):
+        """
+        @param schema: A schema object.
+        @type schema: L{xsd.schema.Schema}
+        """
+        TreeResolver.__init__(self, schema)
+
+    def find(self, name, object, resolved=False, push=True):
+        """
+        @param name: The name of the object to be resolved.
+        @type name: basestring
+        @param object: The name's value.
+        @type object: (any|L{Object})
+        @param resolved: A flag indicating that the fully resolved type
+            should be returned.
+        @type resolved: boolean
+        @param push: Indicates that the resolved type should be
+            pushed onto the stack.
+        @type push: boolean
+        @return: The found schema I{type}
+        @rtype: L{xsd.sxbase.SchemaObject}
+        """
+        known = None
+        parent = self.top().resolved
+        if parent is None:
+            result, ancestry = self.query(name)
+        else:
+            result, ancestry = self.getchild(name, parent)
+        if result is None:
+            return None
+        if isinstance(object, Object):
+            known = self.known(object)
+        if push:
+            frame = Frame(result, resolved=known, ancestry=ancestry)
+            pushed = self.push(frame)
+        if resolved:
+            if known is None:
+                result = result.resolve()
+            else:
+                result = known
+        return result
+
+    def query(self, name):
+        """Blindly query the schema by name."""
+        log.debug('searching schema for (%s)', name)
+        schema = self.schema
+        wsdl = self.wsdl()
+        if wsdl is None:
+            qref = qualify(name, schema.root, schema.tns)
+        else:
+            qref = qualify(name, wsdl.root, wsdl.tns)
+        query = BlindQuery(qref)
+        result = query.execute(schema)
+        return (result, [])
+
+    def wsdl(self):
+        """Get the wsdl."""
+        container = self.schema.container
+        if container is None:
+            return None
+        else:
+            return container.wsdl
+
+    def known(self, object):
+        """Get the type specified in the object's metadata."""
+        try:
+            md = object.__metadata__
+            known = md.sxtype
+            return known
+        except:
+            pass
+
+
+class Frame:
+    def __init__(self, type, resolved=None, ancestry=()):
+        self.type = type
+        if resolved is None:
+            resolved = type.resolve()
+        self.resolved = resolved.resolve()
+        self.ancestry = ancestry
+
+    def __str__(self):
+        return '%s\n%s\n%s' % \
+            (Repr(self.type),
+            Repr(self.resolved),
+            [Repr(t) for t in self.ancestry])
+
+    class Empty:
+        def __getattr__(self, name):
+            if name == 'ancestry':
+                return ()
+            else:
+                return None
+
+
+class Stack(list):
+    def __repr__(self):
+        result = []
+        for item in self:
+            result.append(repr(item))
+        return '\n'.join(result)
diff --git a/suds/sax/__init__.py b/suds/sax/__init__.py
new file mode 100644
index 00000000..be138239
--- /dev/null
+++ b/suds/sax/__init__.py
@@ -0,0 +1,106 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The sax module contains a collection of classes that provide a (D)ocument
+(O)bject (M)odel representation of an XML document. The goal is to provide an
+easy, intuitive interface for managing XML documents. Although, the term, DOM,
+is used above, this model is B{far} better.
+
+XML namespaces in suds are represented using a (2) element tuple containing the
+prefix and the URI, e.g. I{('tns', 'http://myns')}
+
+@var encoder: A I{pluggable} XML special character processor used to encode/
+    decode strings.
+@type encoder: L{Encoder}
+"""
+
+from suds.sax.enc import Encoder
+
+#
+# pluggable XML special character encoder.
+#
+encoder = Encoder()
+
+
+def splitPrefix(name):
+    """
+    Split the name into a tuple (I{prefix}, I{name}). The first element in the
+    tuple is I{None} when the name does not have a prefix.
+    @param name: A node name containing an optional prefix.
+    @type name: basestring
+    @return: A tuple containing the (2) parts of I{name}
+    @rtype: (I{prefix}, I{name})
+    """
+    if isinstance(name, basestring) and ':' in name:
+        return tuple(name.split(':', 1))
+    return None, name
+
+
+class Namespace:
+    """
+    The namespace class represents XML namespaces.
+    """
+
+    default = (None, None)
+    xmlns = ('xml', 'http://www.w3.org/XML/1998/namespace')
+    xsdns = ('xs', 'http://www.w3.org/2001/XMLSchema')
+    xsins = ('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
+    all = (xsdns, xsins)
+
+    @classmethod
+    def create(cls, p=None, u=None):
+        return p, u
+
+    @classmethod
+    def none(cls, ns):
+        return ns == cls.default
+
+    @classmethod
+    def xsd(cls, ns):
+        try:
+            return cls.w3(ns) and ns[1].endswith('XMLSchema')
+        except:
+            pass
+        return False
+
+    @classmethod
+    def xsi(cls, ns):
+        try:
+            return cls.w3(ns) and ns[1].endswith('XMLSchema-instance')
+        except:
+            pass
+        return False
+
+    @classmethod
+    def xs(cls, ns):
+        return cls.xsd(ns) or cls.xsi(ns)
+
+    @classmethod
+    def w3(cls, ns):
+        try:
+            return ns[1].startswith('http://www.w3.org')
+        except:
+            pass
+        return False
+
+    @classmethod
+    def isns(cls, ns):
+        try:
+            return isinstance(ns, tuple) and len(ns) == len(cls.default)
+        except:
+            pass
+        return False
diff --git a/suds/sax/attribute.py b/suds/sax/attribute.py
new file mode 100644
index 00000000..61c5ad7a
--- /dev/null
+++ b/suds/sax/attribute.py
@@ -0,0 +1,174 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides XML I{attribute} classes.
+"""
+
+from suds import *
+from suds.sax import *
+from suds.sax.text import Text
+
+
+class Attribute(UnicodeMixin):
+    """
+    An XML attribute object.
+    @ivar parent: The node containing this attribute
+    @type parent: L{element.Element}
+    @ivar prefix: The I{optional} namespace prefix.
+    @type prefix: basestring
+    @ivar name: The I{unqualified} name of the attribute
+    @type name: basestring
+    @ivar value: The attribute's value
+    @type value: basestring
+    """
+    def __init__(self, name, value=None):
+        """
+        @param name: The attribute's name with I{optional} namespace prefix.
+        @type name: basestring
+        @param value: The attribute's value
+        @type value: basestring
+        """
+        self.parent = None
+        self.prefix, self.name = splitPrefix(name)
+        self.setValue(value)
+
+    def clone(self, parent=None):
+        """
+        Clone this object.
+        @param parent: The parent for the clone.
+        @type parent: L{element.Element}
+        @return: A copy of this object assigned to the new parent.
+        @rtype: L{Attribute}
+        """
+        a = Attribute(self.qname(), self.value)
+        a.parent = parent
+        return a
+
+    def qname(self):
+        """
+        Get the B{fully} qualified name of this attribute
+        @return: The fully qualified name.
+        @rtype: basestring
+        """
+        if self.prefix is None:
+            return self.name
+        else:
+            return ':'.join((self.prefix, self.name))
+
+    def setValue(self, value):
+        """
+        Set the attributes value
+        @param value: The new value (may be None)
+        @type value: basestring
+        @return: self
+        @rtype: L{Attribute}
+        """
+        if isinstance(value, Text):
+            self.value = value
+        else:
+            self.value = Text(value)
+        return self
+
+    def getValue(self, default=Text('')):
+        """
+        Get the attributes value with optional default.
+        @param default: An optional value to be return when the
+            attribute's has not been set.
+        @type default: basestring
+        @return: The attribute's value, or I{default}
+        @rtype: L{Text}
+        """
+        if self.hasText():
+            return self.value
+        else:
+            return default
+
+    def hasText(self):
+        """
+        Get whether the attribute has I{text} and that it is not an empty
+        (zero length) string.
+        @return: True when has I{text}.
+        @rtype: boolean
+        """
+        return ( self.value is not None and len(self.value) )
+
+    def namespace(self):
+        """
+        Get the attributes namespace.  This may either be the namespace
+        defined by an optional prefix, or its parent's namespace.
+        @return: The attribute's namespace
+        @rtype: (I{prefix}, I{name})
+        """
+        if self.prefix is None:
+            return Namespace.default
+        else:
+            return self.resolvePrefix(self.prefix)
+
+    def resolvePrefix(self, prefix):
+        """
+        Resolve the specified prefix to a known namespace.
+        @param prefix: A declared prefix
+        @type prefix: basestring
+        @return: The namespace that has been mapped to I{prefix}
+        @rtype: (I{prefix}, I{name})
+        """
+        ns = Namespace.default
+        if self.parent is not None:
+            ns = self.parent.resolvePrefix(prefix)
+        return ns
+
+    def match(self, name=None, ns=None):
+        """
+        Match by (optional) name and/or (optional) namespace.
+        @param name: The optional attribute tag name.
+        @type name: str
+        @param ns: An optional namespace.
+        @type ns: (I{prefix}, I{name})
+        @return: True if matched.
+        @rtype: boolean
+        """
+        if name is None:
+            byname = True
+        else:
+            byname = ( self.name == name )
+        if ns is None:
+            byns = True
+        else:
+            byns = ( self.namespace()[1] == ns[1] )
+        return ( byname and byns )
+
+    def __eq__(self, rhs):
+        """ equals operator """
+        return rhs is not None and \
+            isinstance(rhs, Attribute) and \
+            self.prefix == rhs.name and \
+            self.name == rhs.name
+
+    def __repr__(self):
+        """ get a string representation """
+        return \
+            'attr (prefix=%s, name=%s, value=(%s))' %\
+                (self.prefix, self.name, self.value)
+
+    def __unicode__(self):
+        """ get an xml string representation """
+        n = self.qname()
+        if self.hasText():
+            v = self.value.escape()
+        else:
+            v = self.value
+        return u'%s="%s"' % (n, v)
diff --git a/suds/sax/date.py b/suds/sax/date.py
new file mode 100644
index 00000000..c5e5e93d
--- /dev/null
+++ b/suds/sax/date.py
@@ -0,0 +1,458 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic@pke.hr )
+# based on code by: Glen Walker
+# based on code by: Nathan Van Gheem ( vangheem@gmail.com )
+
+"""Classes for conversion between XML dates and Python objects."""
+
+from suds import UnicodeMixin
+
+import datetime
+import re
+import time
+
+
+_SNIPPET_DATE =  \
+    r"(?P<year>\d{1,})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
+_SNIPPET_TIME =  \
+    r"(?P<hour>\d{1,2}):(?P<minute>[0-5]?[0-9]):(?P<second>[0-5]?[0-9])"  \
+    r"(?:\.(?P<subsecond>\d+))?"
+_SNIPPET_ZONE =  \
+    r"(?:(?P<tz_sign>[-+])(?P<tz_hour>\d{1,2})"  \
+    r"(?::(?P<tz_minute>[0-5]?[0-9]))?)"  \
+    r"|(?P<tz_utc>[Zz])"
+
+_PATTERN_DATE = r"^%s(?:%s)?$" % (_SNIPPET_DATE, _SNIPPET_ZONE)
+_PATTERN_TIME = r"^%s(?:%s)?$" % (_SNIPPET_TIME, _SNIPPET_ZONE)
+_PATTERN_DATETIME = r"^%s[T ]%s(?:%s)?$" % (_SNIPPET_DATE, _SNIPPET_TIME,
+                                            _SNIPPET_ZONE)
+
+_RE_DATE = re.compile(_PATTERN_DATE)
+_RE_TIME = re.compile(_PATTERN_TIME)
+_RE_DATETIME = re.compile(_PATTERN_DATETIME)
+
+
+class Date(UnicodeMixin):
+    """
+    An XML date object supporting the xsd:date datatype.
+
+    @ivar value: The object value.
+    @type value: B{datetime}.I{date}
+
+    """
+
+    def __init__(self, value):
+        """
+        @param value: The date value of the object.
+        @type value: (datetime.date|str)
+        @raise ValueError: When I{value} is invalid.
+
+        """
+        if isinstance(value, datetime.datetime):
+            self.value = value.date()
+        elif isinstance(value, datetime.date):
+            self.value = value
+        elif isinstance(value, basestring):
+            self.value = self.__parse(value)
+        else:
+            raise ValueError("invalid type for Date(): %s" % type(value))
+
+    @staticmethod
+    def __parse(value):
+        """
+        Parse the string date.
+
+        Supports the subset of ISO8601 used by xsd:date, but is lenient with
+        what is accepted, handling most reasonable syntax.
+
+        Any timezone is parsed but ignored because a) it is meaningless without
+        a time and b) B{datetime}.I{date} does not support timezone
+        information.
+
+        @param value: A date string.
+        @type value: str
+        @return: A date object.
+        @rtype: B{datetime}.I{date}
+
+        """
+        match_result = _RE_DATE.match(value)
+        if match_result is None:
+            raise ValueError("date data has invalid format '%s'" % (value,))
+        return _date_from_match(match_result)
+
+    def __unicode__(self):
+        return self.value.isoformat()
+
+
+class DateTime(UnicodeMixin):
+    """
+    An XML datetime object supporting the xsd:dateTime datatype.
+
+    @ivar value: The object value.
+    @type value: B{datetime}.I{datetime}
+
+    """
+
+    def __init__(self, value):
+        """
+        @param value: The datetime value of the object.
+        @type value: (datetime.datetime|str)
+        @raise ValueError: When I{value} is invalid.
+
+        """
+        if isinstance(value, datetime.datetime):
+            self.value = value
+        elif isinstance(value, basestring):
+            self.value = self.__parse(value)
+        else:
+            raise ValueError("invalid type for DateTime(): %s" % type(value))
+
+    @staticmethod
+    def __parse(value):
+        """
+        Parse the string datetime.
+
+        Supports the subset of ISO8601 used by xsd:dateTime, but is lenient
+        with what is accepted, handling most reasonable syntax.
+
+        Subsecond information is rounded to microseconds due to a restriction
+        in the python datetime.datetime/time implementation.
+
+        @param value: A datetime string.
+        @type value: str
+        @return: A datetime object.
+        @rtype: B{datetime}.I{datetime}
+
+        """
+        match_result = _RE_DATETIME.match(value)
+        if match_result is None:
+           raise ValueError("date data has invalid format '%s'" % (value,))
+
+        date = _date_from_match(match_result)
+        time, round_up = _time_from_match(match_result)
+        tzinfo = _tzinfo_from_match(match_result)
+
+        value = datetime.datetime.combine(date, time)
+        value = value.replace(tzinfo=tzinfo)
+        if round_up:
+            value += datetime.timedelta(microseconds=1)
+        return value
+
+    def __unicode__(self):
+        return self.value.isoformat()
+
+
+class Time(UnicodeMixin):
+    """
+    An XML time object supporting the xsd:time datatype.
+
+    @ivar value: The object value.
+    @type value: B{datetime}.I{time}
+
+    """
+
+    def __init__(self, value):
+        """
+        @param value: The time value of the object.
+        @type value: (datetime.time|str)
+        @raise ValueError: When I{value} is invalid.
+
+        """
+        if isinstance(value, datetime.time):
+            self.value = value
+        elif isinstance(value, basestring):
+            self.value = self.__parse(value)
+        else:
+            raise ValueError("invalid type for Time(): %s" % type(value))
+
+    @staticmethod
+    def __parse(value):
+        """
+        Parse the string date.
+
+        Supports the subset of ISO8601 used by xsd:time, but is lenient with
+        what is accepted, handling most reasonable syntax.
+
+        Subsecond information is rounded to microseconds due to a restriction
+        in the python datetime.time implementation.
+
+        @param value: A time string.
+        @type value: str
+        @return: A time object.
+        @rtype: B{datetime}.I{time}
+
+        """
+        match_result = _RE_TIME.match(value)
+        if match_result is None:
+           raise ValueError("date data has invalid format '%s'" % (value,))
+
+        time, round_up = _time_from_match(match_result)
+        tzinfo = _tzinfo_from_match(match_result)
+        if round_up:
+            time = _bump_up_time_by_microsecond(time)
+        return time.replace(tzinfo=tzinfo)
+
+    def __unicode__(self):
+        return self.value.isoformat()
+
+
+class FixedOffsetTimezone(datetime.tzinfo, UnicodeMixin):
+    """
+    A timezone with a fixed offset and no daylight savings adjustment.
+
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+
+    """
+
+    def __init__(self, offset):
+        """
+        @param offset: The fixed offset of the timezone.
+        @type offset: I{int} or B{datetime}.I{timedelta}
+
+        """
+        if type(offset) == int:
+            offset = datetime.timedelta(hours=offset)
+        elif type(offset) != datetime.timedelta:
+            raise TypeError("timezone offset must be an int or "
+                "datetime.timedelta")
+        if offset.microseconds or (offset.seconds % 60 != 0):
+            raise ValueError("timezone offset must have minute precision")
+        self.__offset = offset
+
+    def dst(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.dst
+
+        """
+        return datetime.timedelta(0)
+
+    def utcoffset(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.utcoffset
+
+        """
+        return self.__offset
+
+    def tzname(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
+
+        """
+        # total_seconds was introduced in Python 2.7
+        if hasattr(self.__offset, "total_seconds"):
+            total_seconds = self.__offset.total_seconds()
+        else:
+            total_seconds = (self.__offset.days * 24 * 60 * 60) + \
+                            (self.__offset.seconds)
+
+        hours = total_seconds // (60 * 60)
+        total_seconds -= hours * 60 * 60
+
+        minutes = total_seconds // 60
+        total_seconds -= minutes * 60
+
+        seconds = total_seconds // 1
+        total_seconds -= seconds
+
+        if seconds:
+            return "%+03d:%02d:%02d" % (hours, minutes, seconds)
+        return "%+03d:%02d" % (hours, minutes)
+
+    def __unicode__(self):
+        return "FixedOffsetTimezone %s" % (self.tzname(None),)
+
+
+class UtcTimezone(FixedOffsetTimezone):
+    """
+    The UTC timezone.
+
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+
+    """
+
+    def __init__(self):
+        FixedOffsetTimezone.__init__(self, datetime.timedelta(0))
+
+    def tzname(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
+
+        """
+        return "UTC"
+
+    def __unicode__(self):
+        return "UtcTimezone"
+
+
+class LocalTimezone(datetime.tzinfo):
+    """
+    The local timezone of the operating system.
+
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+
+    """
+
+    def __init__(self):
+        self.__offset = datetime.timedelta(seconds=-time.timezone)
+        self.__dst_offset = None
+        if time.daylight:
+            self.__dst_offset = datetime.timedelta(seconds=-time.altzone)
+
+    def dst(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.dst
+
+        """
+        if self.__is_daylight_time(dt):
+            return self.__dst_offset - self.__offset
+        return datetime.timedelta(0)
+
+    def tzname(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
+
+        """
+        if self.__is_daylight_time(dt):
+            return time.tzname[1]
+        return time.tzname[0]
+
+    def utcoffset(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.utcoffset
+
+        """
+        if self.__is_daylight_time(dt):
+            return self.__dst_offset
+        return self.__offset
+
+    def __is_daylight_time(self, dt):
+        if not time.daylight:
+            return False
+        time_tuple = dt.replace(tzinfo=None).timetuple()
+        time_tuple = time.localtime(time.mktime(time_tuple))
+        return time_tuple.tm_isdst > 0
+
+    def __unicode__(self):
+        dt = datetime.datetime.now()
+        return "LocalTimezone %s offset: %s dst: %s" % (self.tzname(dt),
+            self.utcoffset(dt), self.dst(dt))
+
+
+def _bump_up_time_by_microsecond(time):
+    """
+    Helper function bumping up the given datetime.time by a microsecond,
+    cycling around silently to 00:00:00.0 in case of an overflow.
+
+    @param time: Time object.
+    @type value: B{datetime}.I{time}
+    @return: Time object.
+    @rtype: B{datetime}.I{time}
+
+    """
+    dt = datetime.datetime(2000, 1, 1, time.hour, time.minute,
+        time.second, time.microsecond)
+    dt += datetime.timedelta(microseconds=1)
+    return dt.time()
+
+
+def _date_from_match(match_object):
+    """
+    Create a date object from a regular expression match.
+
+    The regular expression match is expected to be from _RE_DATE or
+    _RE_DATETIME.
+
+    @param match_object: The regular expression match.
+    @type value: B{re}.I{MatchObject}
+    @return: A date object.
+    @rtype: B{datetime}.I{date}
+
+    """
+    year = int(match_object.group("year"))
+    month = int(match_object.group("month"))
+    day = int(match_object.group("day"))
+    return datetime.date(year, month, day)
+
+
+def _time_from_match(match_object):
+    """
+    Create a time object from a regular expression match.
+
+    Returns the time object and information whether the resulting time should
+    be bumped up by one microsecond due to microsecond rounding.
+
+    Subsecond information is rounded to microseconds due to a restriction in
+    the python datetime.datetime/time implementation.
+
+    The regular expression match is expected to be from _RE_DATETIME or
+    _RE_TIME.
+
+    @param match_object: The regular expression match.
+    @type value: B{re}.I{MatchObject}
+    @return: Time object + rounding flag.
+    @rtype: tuple of B{datetime}.I{time} and bool
+
+    """
+    hour = int(match_object.group('hour'))
+    minute = int(match_object.group('minute'))
+    second = int(match_object.group('second'))
+    subsecond = match_object.group('subsecond')
+
+    round_up = False
+    microsecond = 0
+    if subsecond:
+        round_up = len(subsecond) > 6 and int(subsecond[6]) >= 5
+        subsecond = subsecond[:6]
+        microsecond = int(subsecond + "0" * (6 - len(subsecond)))
+    return datetime.time(hour, minute, second, microsecond), round_up
+
+
+def _tzinfo_from_match(match_object):
+    """
+    Create a timezone information object from a regular expression match.
+
+    The regular expression match is expected to be from _RE_DATE, _RE_DATETIME
+    or _RE_TIME.
+
+    @param match_object: The regular expression match.
+    @type value: B{re}.I{MatchObject}
+    @return: A timezone information object.
+    @rtype: B{datetime}.I{tzinfo}
+
+    """
+    tz_utc = match_object.group("tz_utc")
+    if tz_utc:
+        return UtcTimezone()
+
+    tz_sign = match_object.group("tz_sign")
+    if not tz_sign:
+        return
+
+    h = int(match_object.group("tz_hour") or 0)
+    m = int(match_object.group("tz_minute") or 0)
+    if h == 0 and m == 0:
+        return UtcTimezone()
+
+    # Python limitation - timezone offsets larger than one day (in absolute)
+    # will cause operations depending on tzinfo.utcoffset() to fail, e.g.
+    # comparing two timezone aware datetime.datetime/time objects.
+    if h >= 24:
+        raise ValueError("timezone indicator too large")
+
+    tz_delta = datetime.timedelta(hours=h, minutes=m)
+    if tz_sign == "-":
+        tz_delta *= -1
+    return FixedOffsetTimezone(tz_delta)
diff --git a/suds/sax/document.py b/suds/sax/document.py
new file mode 100644
index 00000000..7a4a615d
--- /dev/null
+++ b/suds/sax/document.py
@@ -0,0 +1,176 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides XML I{document} classes.
+"""
+
+from suds import *
+from suds.sax import *
+from suds.sax.element import Element
+
+
+class Document:
+    """ An XML Document """
+
+    DECL = '<?xml version="1.0" encoding="UTF-8"?>'
+
+    def __init__(self, root=None):
+        """
+        @param root: A root L{Element} or name used to build
+            the document root element.
+        @type root: (L{Element}|str|None)
+        """
+        self.__root = None
+        self.append(root)
+
+    def root(self):
+        """
+        Get the document root element (can be None)
+        @return: The document root.
+        @rtype: L{Element}
+        """
+        return self.__root
+
+    def append(self, node):
+        """
+        Append (set) the document root.
+        @param node: A root L{Element} or name used to build
+            the document root element.
+        @type node: (L{Element}|str|None)
+        """
+        if isinstance(node, basestring):
+            self.__root = Element(node)
+            return
+        if isinstance(node, Element):
+            self.__root = node
+            return
+
+    def getChild(self, name, ns=None, default=None):
+        """
+        Get a child by (optional) name and/or (optional) namespace.
+        @param name: The name of a child element (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace used to match the child.
+        @type ns: (I{prefix}, I{name})
+        @param default: Returned when child not-found.
+        @type default: L{Element}
+        @return: The requested child, or I{default} when not-found.
+        @rtype: L{Element}
+        """
+        if self.__root is None:
+            return default
+        if ns is None:
+            prefix, name = splitPrefix(name)
+            if prefix is None:
+                ns = None
+            else:
+                ns = self.__root.resolvePrefix(prefix)
+        if self.__root.match(name, ns):
+            return self.__root
+        else:
+            return default
+
+    def childAtPath(self, path):
+        """
+        Get a child at I{path} where I{path} is a (/) separated
+        list of element names that are expected to be children.
+        @param path: A (/) separated list of element names.
+        @type path: basestring
+        @return: The leaf node at the end of I{path}
+        @rtype: L{Element}
+        """
+        if self.__root is None:
+            return None
+        if path[0] == '/':
+            path = path[1:]
+        path = path.split('/',1)
+        if self.getChild(path[0]) is None:
+            return None
+        if len(path) > 1:
+            return self.__root.childAtPath(path[1])
+        else:
+            return self.__root
+
+    def childrenAtPath(self, path):
+        """
+        Get a list of children at I{path} where I{path} is a (/) separated
+        list of element names that are expected to be children.
+        @param path: A (/) separated list of element names.
+        @type path: basestring
+        @return: The collection leaf nodes at the end of I{path}
+        @rtype: [L{Element},...]
+        """
+        if self.__root is None:
+            return []
+        if path[0] == '/':
+            path = path[1:]
+        path = path.split('/',1)
+        if self.getChild(path[0]) is None:
+            return []
+        if len(path) > 1:
+            return self.__root.childrenAtPath(path[1])
+        else:
+            return [self.__root,]
+
+    def getChildren(self, name=None, ns=None):
+        """
+        Get a list of children by (optional) name and/or (optional) namespace.
+        @param name: The name of a child element (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace used to match the child.
+        @type ns: (I{prefix}, I{name})
+        @return: The list of matching children.
+        @rtype: [L{Element},...]
+        """
+        if name is None:
+            matched = self.__root
+        else:
+            matched = self.getChild(name, ns)
+        if matched is None:
+            return []
+        else:
+            return [matched,]
+
+    def str(self):
+        """
+        Get a string representation of this XML document.
+        @return: A I{pretty} string.
+        @rtype: basestring
+        """
+        s = []
+        s.append(self.DECL)
+        root = self.root()
+        if root is not None:
+            s.append('\n')
+            s.append(root.str())
+        return ''.join(s)
+
+    def plain(self):
+        """
+        Get a string representation of this XML document.
+        @return: A I{plain} string.
+        @rtype: basestring
+        """
+        s = []
+        s.append(self.DECL)
+        root = self.root()
+        if root is not None:
+            s.append(root.plain())
+        return ''.join(s)
+
+    def __unicode__(self):
+        return self.str()
diff --git a/suds/sax/element.py b/suds/sax/element.py
new file mode 100644
index 00000000..084ea2b0
--- /dev/null
+++ b/suds/sax/element.py
@@ -0,0 +1,1106 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides XML I{element} classes.
+"""
+
+from suds import *
+from suds.sax import *
+from suds.sax.text import Text
+from suds.sax.attribute import Attribute
+
+
+class Element(UnicodeMixin):
+    """
+    An XML element object.
+    @ivar parent: The node containing this attribute
+    @type parent: L{Element}
+    @ivar prefix: The I{optional} namespace prefix.
+    @type prefix: basestring
+    @ivar name: The I{unqualified} name of the attribute
+    @type name: basestring
+    @ivar expns: An explicit namespace (xmlns="...").
+    @type expns: (I{prefix}, I{name})
+    @ivar nsprefixes: A mapping of prefixes to namespaces.
+    @type nsprefixes: dict
+    @ivar attributes: A list of XML attributes.
+    @type attributes: [I{Attribute},]
+    @ivar text: The element's I{text} content.
+    @type text: basestring
+    @ivar children: A list of child elements.
+    @type children: [I{Element},]
+    @cvar matcher: A collection of I{lambda} for string matching.
+    @cvar specialprefixes: A dictionary of builtin-special prefixes.
+    """
+
+    matcher = {
+        'eq': lambda a,b: a == b,
+        'startswith' : lambda a,b: a.startswith(b),
+        'endswith' : lambda a,b: a.endswith(b),
+        'contains' : lambda a,b: b in a}
+
+    specialprefixes = {Namespace.xmlns[0] : Namespace.xmlns[1]}
+
+    @classmethod
+    def buildPath(self, parent, path):
+        """Build the specifed path as a/b/c.
+
+        Any missing intermediate nodes are built automatically.
+        @param parent: A parent element on which the path is built.
+        @type parent: I{Element}
+        @param path: A simple path separated by (/).
+        @type path: basestring
+        @return: The leaf node of I{path}.
+        @rtype: L{Element}
+        """
+        for tag in path.split('/'):
+            child = parent.getChild(tag)
+            if child is None:
+                child = Element(tag, parent)
+            parent = child
+        return child
+
+    def __init__(self, name, parent=None, ns=None):
+        """
+        @param name: The element's (tag) name. May contain a prefix.
+        @type name: basestring
+        @param parent: An optional parent element.
+        @type parent: I{Element}
+        @param ns: An optional namespace.
+        @type ns: (I{prefix}, I{name})
+        """
+        self.rename(name)
+        self.expns = None
+        self.nsprefixes = {}
+        self.attributes = []
+        self.text = None
+        if parent is not None:
+            if isinstance(parent, Element):
+                self.parent = parent
+            else:
+                raise Exception('parent (%s) not-valid', parent.__class__.__name__)
+        else:
+            self.parent = None
+        self.children = []
+        self.applyns(ns)
+
+    def rename(self, name):
+        """
+        Rename the element.
+        @param name: A new name for the element.
+        @type name: basestring
+        """
+        if name is None:
+            raise Exception('name (%s) not-valid' % name)
+        else:
+            self.prefix, self.name = splitPrefix(name)
+
+    def setPrefix(self, p, u=None):
+        """
+        Set the element namespace prefix.
+        @param p: A new prefix for the element.
+        @type p: basestring
+        @param u: A namespace URI to be mapped to the prefix.
+        @type u: basestring
+        @return: self
+        @rtype: L{Element}
+        """
+        self.prefix = p
+        if p is not None and u is not None:
+            self.expns = None
+            self.addPrefix(p, u)
+        return self
+
+    def qname(self):
+        """
+        Get the B{fully} qualified name of this element
+        @return: The fully qualified name.
+        @rtype: basestring
+        """
+        if self.prefix is None:
+            return self.name
+        return '%s:%s' % (self.prefix, self.name)
+
+    def getRoot(self):
+        """
+        Get the root (top) node of the tree.
+        @return: The I{top} node of this tree.
+        @rtype: I{Element}
+        """
+        if self.parent is None:
+            return self
+        return self.parent.getRoot()
+
+    def clone(self, parent=None):
+        """
+        Deep clone of this element and children.
+        @param parent: An optional parent for the copied fragment.
+        @type parent: I{Element}
+        @return: A deep copy parented by I{parent}
+        @rtype: I{Element}
+        """
+        root = Element(self.qname(), parent, self.namespace())
+        for a in self.attributes:
+            root.append(a.clone(self))
+        for c in self.children:
+            root.append(c.clone(self))
+        for item in self.nsprefixes.items():
+            root.addPrefix(item[0], item[1])
+        return root
+
+    def detach(self):
+        """
+        Detach from parent.
+        @return: This element removed from its parent's
+            child list and I{parent}=I{None}
+        @rtype: L{Element}
+        """
+        if self.parent is not None:
+            if self in self.parent.children:
+                self.parent.children.remove(self)
+            self.parent = None
+        return self
+
+    def set(self, name, value):
+        """
+        Set an attribute's value.
+        @param name: The name of the attribute.
+        @type name: basestring
+        @param value: The attribute value.
+        @type value: basestring
+        @see: __setitem__()
+        """
+        attr = self.getAttribute(name)
+        if attr is None:
+            attr = Attribute(name, value)
+            self.append(attr)
+        else:
+            attr.setValue(value)
+
+    def unset(self, name):
+        """
+        Unset (remove) an attribute.
+        @param name: The attribute name.
+        @type name: str
+        @return: self
+        @rtype: L{Element}
+        """
+        try:
+            attr = self.getAttribute(name)
+            self.attributes.remove(attr)
+        except:
+            pass
+        return self
+
+    def get(self, name, ns=None, default=None):
+        """
+        Get the value of an attribute by name.
+        @param name: The name of the attribute.
+        @type name: basestring
+        @param ns: The optional attribute's namespace.
+        @type ns: (I{prefix}, I{name})
+        @param default: An optional value to be returned when either
+            the attribute does not exist of has not value.
+        @type default: basestring
+        @return: The attribute's value or I{default}
+        @rtype: basestring
+        @see: __getitem__()
+        """
+        attr = self.getAttribute(name, ns)
+        if attr is None or attr.value is None:
+            return default
+        return attr.getValue()
+
+    def setText(self, value):
+        """
+        Set the element's L{Text} content.
+        @param value: The element's text value.
+        @type value: basestring
+        @return: self
+        @rtype: I{Element}
+        """
+        if isinstance(value, Text):
+            self.text = value
+        else:
+            self.text = Text(value)
+        return self
+
+    def getText(self, default=None):
+        """
+        Get the element's L{Text} content with optional default
+        @param default: A value to be returned when no text content exists.
+        @type default: basestring
+        @return: The text content, or I{default}
+        @rtype: L{Text}
+        """
+        if self.hasText():
+            return self.text
+        return default
+
+    def trim(self):
+        """
+        Trim leading and trailing whitespace.
+        @return: self
+        @rtype: L{Element}
+        """
+        if self.hasText():
+            self.text = self.text.trim()
+        return self
+
+    def hasText(self):
+        """
+        Get whether the element has I{text} and that it is not an empty
+        (zero length) string.
+        @return: True when has I{text}.
+        @rtype: boolean
+        """
+        return self.text is not None and len(self.text)
+
+    def namespace(self):
+        """
+        Get the element's namespace.
+        @return: The element's namespace by resolving the prefix, the explicit
+            namespace or the inherited namespace.
+        @rtype: (I{prefix}, I{name})
+        """
+        if self.prefix is None:
+            return self.defaultNamespace()
+        return self.resolvePrefix(self.prefix)
+
+    def defaultNamespace(self):
+        """
+        Get the default (unqualified namespace).
+        This is the expns of the first node (looking up the tree)
+        that has it set.
+        @return: The namespace of a node when not qualified.
+        @rtype: (I{prefix}, I{name})
+        """
+        p = self
+        while p is not None:
+            if p.expns is not None:
+                return None, p.expns
+            p = p.parent
+        return Namespace.default
+
+    def append(self, objects):
+        """
+        Append the specified child based on whether it is an element or an
+        attribute.
+        @param objects: A (single|collection) of attribute(s) or element(s)
+            to be added as children.
+        @type objects: (L{Element}|L{Attribute})
+        @return: self
+        @rtype: L{Element}
+        """
+        if not isinstance(objects, (list, tuple)):
+            objects = (objects,)
+        for child in objects:
+            if isinstance(child, Element):
+                self.children.append(child)
+                child.parent = self
+                continue
+            if isinstance(child, Attribute):
+                self.attributes.append(child)
+                child.parent = self
+                continue
+            raise Exception('append %s not-valid' % child.__class__.__name__)
+        return self
+
+    def insert(self, objects, index=0):
+        """
+        Insert an L{Element} content at the specified index.
+        @param objects: A (single|collection) of attribute(s) or element(s) to
+            be added as children.
+        @type objects: (L{Element}|L{Attribute})
+        @param index: The position in the list of children to insert.
+        @type index: int
+        @return: self
+        @rtype: L{Element}
+        """
+        objects = (objects,)
+        for child in objects:
+            if isinstance(child, Element):
+                self.children.insert(index, child)
+                child.parent = self
+            else:
+                raise Exception('append %s not-valid' % child.__class__.__name__)
+        return self
+
+    def remove(self, child):
+        """
+        Remove the specified child element or attribute.
+        @param child: A child to remove.
+        @type child: L{Element}|L{Attribute}
+        @return: The detached I{child} when I{child} is an element, else None.
+        @rtype: L{Element}|None
+        """
+        if isinstance(child, Element):
+            return child.detach()
+        if isinstance(child, Attribute):
+            self.attributes.remove(child)
+        return None
+
+    def replaceChild(self, child, content):
+        """
+        Replace I{child} with the specified I{content}.
+        @param child: A child element.
+        @type child: L{Element}
+        @param content: An element or collection of elements.
+        @type content: L{Element} or [L{Element},]
+        """
+        if child not in self.children:
+            raise Exception('child not-found')
+        index = self.children.index(child)
+        self.remove(child)
+        if not isinstance(content, (list, tuple)):
+            content = (content,)
+        for node in content:
+            self.children.insert(index, node.detach())
+            node.parent = self
+            index += 1
+
+    def getAttribute(self, name, ns=None, default=None):
+        """
+        Get an attribute by name and (optional) namespace
+        @param name: The name of a contained attribute (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace
+        @type ns: (I{prefix}, I{name})
+        @param default: Returned when attribute not-found.
+        @type default: L{Attribute}
+        @return: The requested attribute object.
+        @rtype: L{Attribute}
+        """
+        if ns is None:
+            prefix, name = splitPrefix(name)
+            if prefix is None:
+                ns = None
+            else:
+                ns = self.resolvePrefix(prefix)
+        for a in self.attributes:
+            if a.match(name, ns):
+                return a
+        return default
+
+    def getChild(self, name, ns=None, default=None):
+        """
+        Get a child by (optional) name and/or (optional) namespace.
+        @param name: The name of a child element (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace used to match the child.
+        @type ns: (I{prefix}, I{name})
+        @param default: Returned when child not-found.
+        @type default: L{Element}
+        @return: The requested child, or I{default} when not-found.
+        @rtype: L{Element}
+        """
+        if ns is None:
+            prefix, name = splitPrefix(name)
+            if prefix is None:
+                ns = None
+            else:
+                ns = self.resolvePrefix(prefix)
+        for c in self.children:
+            if c.match(name, ns):
+                return c
+        return default
+
+    def childAtPath(self, path):
+        """
+        Get a child at I{path} where I{path} is a (/) separated
+        list of element names that are expected to be children.
+        @param path: A (/) separated list of element names.
+        @type path: basestring
+        @return: The leaf node at the end of I{path}
+        @rtype: L{Element}
+        """
+        result = None
+        node = self
+        for name in [p for p in path.split('/') if len(p) > 0]:
+            ns = None
+            prefix, name = splitPrefix(name)
+            if prefix is not None:
+                ns = node.resolvePrefix(prefix)
+            result = node.getChild(name, ns)
+            if result is None:
+                break;
+            else:
+                node = result
+        return result
+
+    def childrenAtPath(self, path):
+        """
+        Get a list of children at I{path} where I{path} is a (/) separated
+        list of element names that are expected to be children.
+        @param path: A (/) separated list of element names.
+        @type path: basestring
+        @return: The collection leaf nodes at the end of I{path}
+        @rtype: [L{Element},...]
+        """
+        parts = [p for p in path.split('/') if len(p) > 0]
+        if len(parts) == 1:
+            result = self.getChildren(path)
+        else:
+            result = self.__childrenAtPath(parts)
+        return result
+
+    def getChildren(self, name=None, ns=None):
+        """
+        Get a list of children by (optional) name and/or (optional) namespace.
+        @param name: The name of a child element (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace used to match the child.
+        @type ns: (I{prefix}, I{name})
+        @return: The list of matching children.
+        @rtype: [L{Element},...]
+        """
+        if ns is None:
+            if name is None:
+                return self.children
+            prefix, name = splitPrefix(name)
+            if prefix is None:
+                ns = None
+            else:
+                ns = self.resolvePrefix(prefix)
+        return [c for c in self.children if c.match(name, ns)]
+
+    def detachChildren(self):
+        """
+        Detach and return this element's children.
+        @return: The element's children (detached).
+        @rtype: [L{Element},...]
+        """
+        detached = self.children
+        self.children = []
+        for child in detached:
+            child.parent = None
+        return detached
+
+    def resolvePrefix(self, prefix, default=Namespace.default):
+        """
+        Resolve the specified prefix to a namespace.  The I{nsprefixes} is
+        searched.  If not found, it walks up the tree until either resolved or
+        the top of the tree is reached.  Searching up the tree provides for
+        inherited mappings.
+        @param prefix: A namespace prefix to resolve.
+        @type prefix: basestring
+        @param default: An optional value to be returned when the prefix
+            cannot be resolved.
+        @type default: (I{prefix},I{URI})
+        @return: The namespace that is mapped to I{prefix} in this context.
+        @rtype: (I{prefix},I{URI})
+        """
+        n = self
+        while n is not None:
+            if prefix in n.nsprefixes:
+                return prefix, n.nsprefixes[prefix]
+            if prefix in self.specialprefixes:
+                return prefix, self.specialprefixes[prefix]
+            n = n.parent
+        return default
+
+    def addPrefix(self, p, u):
+        """
+        Add or update a prefix mapping.
+        @param p: A prefix.
+        @type p: basestring
+        @param u: A namespace URI.
+        @type u: basestring
+        @return: self
+        @rtype: L{Element}
+        """
+        self.nsprefixes[p] = u
+        return self
+
+    def updatePrefix(self, p, u):
+        """
+        Update (redefine) a prefix mapping for the branch.
+        @param p: A prefix.
+        @type p: basestring
+        @param u: A namespace URI.
+        @type u: basestring
+        @return: self
+        @rtype: L{Element}
+        @note: This method traverses down the entire branch!
+        """
+        if p in self.nsprefixes:
+            self.nsprefixes[p] = u
+        for c in self.children:
+            c.updatePrefix(p, u)
+        return self
+
+    def clearPrefix(self, prefix):
+        """
+        Clear the specified prefix from the prefix mappings.
+        @param prefix: A prefix to clear.
+        @type prefix: basestring
+        @return: self
+        @rtype: L{Element}
+        """
+        if prefix in self.nsprefixes:
+            del self.nsprefixes[prefix]
+        return self
+
+    def findPrefix(self, uri, default=None):
+        """
+        Find the first prefix that has been mapped to a namespace URI.
+        The local mapping is searched, then it walks up the tree until
+        it reaches the top or finds a match.
+        @param uri: A namespace URI.
+        @type uri: basestring
+        @param default: A default prefix when not found.
+        @type default: basestring
+        @return: A mapped prefix.
+        @rtype: basestring
+        """
+        for item in self.nsprefixes.items():
+            if item[1] == uri:
+                prefix = item[0]
+                return prefix
+        for item in self.specialprefixes.items():
+            if item[1] == uri:
+                prefix = item[0]
+                return prefix
+        if self.parent is not None:
+            return self.parent.findPrefix(uri, default)
+        return default
+
+    def findPrefixes(self, uri, match='eq'):
+        """
+        Find all prefixes that have been mapped to a namespace URI.
+        The local mapping is searched, then it walks up the tree until it
+        reaches the top, collecting all matches.
+        @param uri: A namespace URI.
+        @type uri: basestring
+        @param match: A matching function L{Element.matcher}.
+        @type match: basestring
+        @return: A list of mapped prefixes.
+        @rtype: [basestring,...]
+        """
+        result = []
+        for item in self.nsprefixes.items():
+            if self.matcher[match](item[1], uri):
+                prefix = item[0]
+                result.append(prefix)
+        for item in self.specialprefixes.items():
+            if self.matcher[match](item[1], uri):
+                prefix = item[0]
+                result.append(prefix)
+        if self.parent is not None:
+            result += self.parent.findPrefixes(uri, match)
+        return result
+
+    def promotePrefixes(self):
+        """
+        Push prefix declarations up the tree as far as possible.  Prefix
+        mapping are pushed to its parent unless the parent has the
+        prefix mapped to another URI or the parent has the prefix.
+        This is propagated up the tree until the top is reached.
+        @return: self
+        @rtype: L{Element}
+        """
+        for c in self.children:
+            c.promotePrefixes()
+        if self.parent is None:
+            return
+        for p,u in self.nsprefixes.items():
+            if p in self.parent.nsprefixes:
+                pu = self.parent.nsprefixes[p]
+                if pu == u:
+                    del self.nsprefixes[p]
+                continue
+            if p != self.parent.prefix:
+                self.parent.nsprefixes[p] = u
+                del self.nsprefixes[p]
+        return self
+
+    def refitPrefixes(self):
+        """
+        Refit namespace qualification by replacing prefixes
+        with explicit namespaces. Also purges prefix mapping table.
+        @return: self
+        @rtype: L{Element}
+        """
+        for c in self.children:
+            c.refitPrefixes()
+        if self.prefix is not None:
+            ns = self.resolvePrefix(self.prefix)
+            if ns[1] is not None:
+                self.expns = ns[1]
+        self.prefix = None
+        self.nsprefixes = {}
+        return self
+
+    def normalizePrefixes(self):
+        """
+        Normalize the namespace prefixes.
+        This generates unique prefixes for all namespaces.  Then retrofits all
+        prefixes and prefix mappings.  Further, it will retrofix attribute values
+        that have values containing (:).
+        @return: self
+        @rtype: L{Element}
+        """
+        PrefixNormalizer.apply(self)
+        return self
+
+    def isempty(self, content=True):
+        """
+        Get whether the element has no children.
+        @param content: Test content (children & text) only.
+        @type content: boolean
+        @return: True when element has not children.
+        @rtype: boolean
+        """
+        noattrs = not len(self.attributes)
+        nochildren = not len(self.children)
+        notext = ( self.text is None )
+        nocontent = ( nochildren and notext )
+        if content:
+            return nocontent
+        return nocontent and noattrs
+
+    def isnil(self):
+        """
+        Get whether the element is I{nil} as defined by having
+        an attribute in the I{xsi:nil="true"}
+        @return: True if I{nil}, else False
+        @rtype: boolean
+        """
+        nilattr = self.getAttribute('nil', ns=Namespace.xsins)
+        return nilattr is not None and ( nilattr.getValue().lower() == 'true' )
+
+    def setnil(self, flag=True):
+        """
+        Set this node to I{nil} as defined by having an
+        attribute I{xsi:nil}=I{flag}.
+        @param flag: A flag indicating how I{xsi:nil} will be set.
+        @type flag: boolean
+        @return: self
+        @rtype: L{Element}
+        """
+        p, u = Namespace.xsins
+        name  = ':'.join((p, 'nil'))
+        self.set(name, str(flag).lower())
+        self.addPrefix(p, u)
+        if flag:
+            self.text = None
+        return self
+
+    def applyns(self, ns):
+        """
+        Apply the namespace to this node.  If the prefix is I{None} then
+        this element's explicit namespace I{expns} is set to the
+        URI defined by I{ns}.  Otherwise, the I{ns} is simply mapped.
+        @param ns: A namespace.
+        @type ns: (I{prefix},I{URI})
+        """
+        if ns is None:
+            return
+        if not isinstance(ns, (tuple, list)):
+            raise Exception('namespace must be tuple')
+        if ns[0] is None:
+            self.expns = ns[1]
+        else:
+            self.prefix = ns[0]
+            self.nsprefixes[ns[0]] = ns[1]
+
+    def str(self, indent=0):
+        """
+        Get a string representation of this XML fragment.
+        @param indent: The indent to be used in formatting the output.
+        @type indent: int
+        @return: A I{pretty} string.
+        @rtype: basestring
+        """
+        tab = '%*s'%(indent*3,'')
+        result = []
+        result.append('%s<%s' % (tab, self.qname()))
+        result.append(self.nsdeclarations())
+        for a in [unicode(a) for a in self.attributes]:
+            result.append(' %s' % a)
+        if self.isempty():
+            result.append('/>')
+            return ''.join(result)
+        result.append('>')
+        if self.hasText():
+            result.append(self.text.escape())
+        for c in self.children:
+            result.append('\n')
+            result.append(c.str(indent+1))
+        if len(self.children):
+            result.append('\n%s' % tab)
+        result.append('</%s>' % self.qname())
+        return ''.join(result)
+
+    def plain(self):
+        """
+        Get a string representation of this XML fragment.
+        @return: A I{plain} string.
+        @rtype: basestring
+        """
+        result = []
+        result.append('<%s' % self.qname())
+        result.append(self.nsdeclarations())
+        for a in [unicode(a) for a in self.attributes]:
+            result.append(' %s' % a)
+        if self.isempty():
+            result.append('/>')
+            return ''.join(result)
+        result.append('>')
+        if self.hasText():
+            result.append(self.text.escape())
+        for c in self.children:
+            result.append(c.plain())
+        result.append('</%s>' % self.qname())
+        return ''.join(result)
+
+    def nsdeclarations(self):
+        """
+        Get a string representation for all namespace declarations
+        as xmlns="" and xmlns:p="".
+        @return: A separated list of declarations.
+        @rtype: basestring
+        """
+        s = []
+        myns = (None, self.expns)
+        if self.parent is None:
+            pns = Namespace.default
+        else:
+            pns = (None, self.parent.expns)
+        if myns[1] != pns[1]:
+            if self.expns is not None:
+                d = ' xmlns="%s"' % self.expns
+                s.append(d)
+        for item in self.nsprefixes.items():
+            (p,u) = item
+            if self.parent is not None:
+                ns = self.parent.resolvePrefix(p)
+                if ns[1] == u: continue
+            d = ' xmlns:%s="%s"' % (p, u)
+            s.append(d)
+        return ''.join(s)
+
+    def match(self, name=None, ns=None):
+        """
+        Match by (optional) name and/or (optional) namespace.
+        @param name: The optional element tag name.
+        @type name: str
+        @param ns: An optional namespace.
+        @type ns: (I{prefix}, I{name})
+        @return: True if matched.
+        @rtype: boolean
+        """
+        byname = name is None or ( self.name == name )
+        byns = ns is None or ( self.namespace()[1] == ns[1] )
+        return byname and byns
+
+    def branch(self):
+        """
+        Get a flattened representation of the branch.
+        @return: A flat list of nodes.
+        @rtype: [L{Element},..]
+        """
+        branch = [self]
+        for c in self.children:
+            branch += c.branch()
+        return branch
+
+    def ancestors(self):
+        """
+        Get a list of ancestors.
+        @return: A list of ancestors.
+        @rtype: [L{Element},..]
+        """
+        ancestors = []
+        p = self.parent
+        while p is not None:
+            ancestors.append(p)
+            p = p.parent
+        return ancestors
+
+    def walk(self, visitor):
+        """
+        Walk the branch and call the visitor function on each node.
+        @param visitor: A function.
+        @return: self
+        @rtype: L{Element}
+        """
+        visitor(self)
+        for c in self.children:
+            c.walk(visitor)
+        return self
+
+    def prune(self):
+        """
+        Prune the branch of empty nodes.
+        """
+        pruned = []
+        for c in self.children:
+            c.prune()
+            if c.isempty(False):
+                pruned.append(c)
+        for p in pruned:
+            self.children.remove(p)
+
+    def __childrenAtPath(self, parts):
+        result = []
+        node = self
+        last = len(parts)-1
+        ancestors = parts[:last]
+        leaf = parts[last]
+        for name in ancestors:
+            ns = None
+            prefix, name = splitPrefix(name)
+            if prefix is not None:
+                ns = node.resolvePrefix(prefix)
+            child = node.getChild(name, ns)
+            if child is None:
+                break
+            else:
+                node = child
+        if child is not None:
+            ns = None
+            prefix, leaf = splitPrefix(leaf)
+            if prefix is not None:
+                ns = node.resolvePrefix(prefix)
+            result = child.getChildren(leaf)
+        return result
+
+    def __len__(self):
+        return len(self.children)
+
+    def __getitem__(self, index):
+        if isinstance(index, basestring):
+            return self.get(index)
+        if index < len(self.children):
+            return self.children[index]
+
+    def __setitem__(self, index, value):
+        if isinstance(index, basestring):
+            self.set(index, value)
+        else:
+            if index < len(self.children) and isinstance(value, Element):
+                self.children.insert(index, value)
+
+    def __eq__(self, rhs):
+        return isinstance(rhs, Element) and  \
+            self.match(rhs.name, rhs.namespace())
+
+    def __repr__(self):
+        return 'Element (prefix=%s, name=%s)' % (self.prefix, self.name)
+
+    def __unicode__(self):
+        return self.str()
+
+    def __iter__(self):
+        return NodeIterator(self)
+
+
+class NodeIterator:
+    """
+    The L{Element} child node iterator.
+    @ivar pos: The current position
+    @type pos: int
+    @ivar children: A list of a child nodes.
+    @type children: [L{Element},..]
+    """
+
+    def __init__(self, parent):
+        """
+        @param parent: An element to iterate.
+        @type parent: L{Element}
+        """
+        self.pos = 0
+        self.children = parent.children
+
+    def next(self):
+        """
+        Get the next child.
+        @return: The next child.
+        @rtype: L{Element}
+        @raise StopIterator: At the end.
+        """
+        try:
+            child = self.children[self.pos]
+            self.pos += 1
+            return child
+        except:
+            raise StopIteration()
+
+
+class PrefixNormalizer:
+    """
+    The prefix normalizer provides namespace prefix normalization.
+    @ivar node: A node to normalize.
+    @type node: L{Element}
+    @ivar branch: The nodes flattened branch.
+    @type branch: [L{Element},..]
+    @ivar namespaces: A unique list of namespaces (URI).
+    @type namespaces: [str,]
+    @ivar prefixes: A reverse dict of prefixes.
+    @type prefixes: {u, p}
+    """
+
+    @classmethod
+    def apply(cls, node):
+        """
+        Normalize the specified node.
+        @param node: A node to normalize.
+        @type node: L{Element}
+        @return: The normalized node.
+        @rtype: L{Element}
+        """
+        pn = PrefixNormalizer(node)
+        return pn.refit()
+
+    def __init__(self, node):
+        """
+        @param node: A node to normalize.
+        @type node: L{Element}
+        """
+        self.node = node
+        self.branch = node.branch()
+        self.namespaces = self.getNamespaces()
+        self.prefixes = self.genPrefixes()
+
+    def getNamespaces(self):
+        """
+        Get the I{unique} set of namespaces referenced in the branch.
+        @return: A set of namespaces.
+        @rtype: set
+        """
+        s = set()
+        for n in self.branch + self.node.ancestors():
+            if self.permit(n.expns):
+                s.add(n.expns)
+            s = s.union(self.pset(n))
+        return s
+
+    def pset(self, n):
+        """
+        Convert the nodes nsprefixes into a set.
+        @param n: A node.
+        @type n: L{Element}
+        @return: A set of namespaces.
+        @rtype: set
+        """
+        s = set()
+        for ns in n.nsprefixes.items():
+            if self.permit(ns):
+                s.add(ns[1])
+        return s
+
+    def genPrefixes(self):
+        """
+        Generate a I{reverse} mapping of unique prefixes for all namespaces.
+        @return: A referse dict of prefixes.
+        @rtype: {u, p}
+        """
+        prefixes = {}
+        n = 0
+        for u in self.namespaces:
+            p = 'ns%d' % n
+            prefixes[u] = p
+            n += 1
+        return prefixes
+
+    def refit(self):
+        """
+        Refit (normalize) the prefixes in the node.
+        """
+        self.refitNodes()
+        self.refitMappings()
+
+    def refitNodes(self):
+        """
+        Refit (normalize) all of the nodes in the branch.
+        """
+        for n in self.branch:
+            if n.prefix is not None:
+                ns = n.namespace()
+                if self.permit(ns):
+                    n.prefix = self.prefixes[ns[1]]
+            self.refitAttrs(n)
+
+    def refitAttrs(self, n):
+        """
+        Refit (normalize) all of the attributes in the node.
+        @param n: A node.
+        @type n: L{Element}
+        """
+        for a in n.attributes:
+            self.refitAddr(a)
+
+    def refitAddr(self, a):
+        """
+        Refit (normalize) the attribute.
+        @param a: An attribute.
+        @type a: L{Attribute}
+        """
+        if a.prefix is not None:
+            ns = a.namespace()
+            if self.permit(ns):
+                a.prefix = self.prefixes[ns[1]]
+        self.refitValue(a)
+
+    def refitValue(self, a):
+        """
+        Refit (normalize) the attribute's value.
+        @param a: An attribute.
+        @type a: L{Attribute}
+        """
+        p,name = splitPrefix(a.getValue())
+        if p is None: return
+        ns = a.resolvePrefix(p)
+        if self.permit(ns):
+            u = ns[1]
+            p = self.prefixes[u]
+            a.setValue(':'.join((p, name)))
+
+    def refitMappings(self):
+        """
+        Refit (normalize) all of the nsprefix mappings.
+        """
+        for n in self.branch:
+            n.nsprefixes = {}
+        n = self.node
+        for u, p in self.prefixes.items():
+            n.addPrefix(p, u)
+
+    def permit(self, ns):
+        """
+        Get whether the I{ns} is to be normalized.
+        @param ns: A namespace.
+        @type ns: (p,u)
+        @return: True if to be included.
+        @rtype: boolean
+        """
+        return not self.skip(ns)
+
+    def skip(self, ns):
+        """
+        Get whether the I{ns} is to B{not} be normalized.
+        @param ns: A namespace.
+        @type ns: (p,u)
+        @return: True if to be skipped.
+        @rtype: boolean
+        """
+        return (ns is None or
+            ns == Namespace.default or
+            ns == Namespace.xsdns or
+            ns == Namespace.xsins or
+            ns == Namespace.xmlns)
diff --git a/suds/sax/enc.py b/suds/sax/enc.py
new file mode 100644
index 00000000..8d3219c8
--- /dev/null
+++ b/suds/sax/enc.py
@@ -0,0 +1,79 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides XML I{special character} encoder classes.
+"""
+
+import re
+
+class Encoder:
+    """
+    An XML special character encoder/decoder.
+    @cvar encodings: A mapping of special characters encoding.
+    @type encodings: [(str,str)]
+    @cvar decodings: A mapping of special characters decoding.
+    @type decodings: [(str,str)]
+    @cvar special: A list of special characters
+    @type special: [char]
+    """
+
+    encodings = \
+        (( '&(?!(amp|lt|gt|quot|apos);)', '&amp;' ),( '<', '&lt;' ),( '>', '&gt;' ),( '"', '&quot;' ),("'", '&apos;' ))
+    decodings = \
+        (( '&lt;', '<' ),( '&gt;', '>' ),( '&quot;', '"' ),( '&apos;', "'" ),( '&amp;', '&' ))
+    special = \
+        ('&', '<', '>', '"', "'")
+
+    def needsEncoding(self, s):
+        """
+        Get whether string I{s} contains special characters.
+        @param s: A string to check.
+        @type s: str
+        @return: True if needs encoding.
+        @rtype: boolean
+        """
+        if isinstance(s, basestring):
+            for c in self.special:
+                if c in s:
+                    return True
+        return False
+
+    def encode(self, s):
+        """
+        Encode special characters found in string I{s}.
+        @param s: A string to encode.
+        @type s: str
+        @return: The encoded string.
+        @rtype: str
+        """
+        if isinstance(s, basestring) and self.needsEncoding(s):
+            for x in self.encodings:
+                s = re.sub(x[0], x[1], s)
+        return s
+
+    def decode(self, s):
+        """
+        Decode special characters encodings found in string I{s}.
+        @param s: A string to decode.
+        @type s: str
+        @return: The decoded string.
+        @rtype: str
+        """
+        if isinstance(s, basestring) and '&' in s:
+            for x in self.decodings:
+                s = s.replace(x[0], x[1])
+        return s
diff --git a/suds/sax/parser.py b/suds/sax/parser.py
new file mode 100644
index 00000000..a82583a0
--- /dev/null
+++ b/suds/sax/parser.py
@@ -0,0 +1,136 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The sax module contains a collection of classes that provide a
+(D)ocument (O)bject (M)odel representation of an XML document.
+The goal is to provide an easy, intuitive interface for managing XML
+documents.  Although, the term, DOM, is used above, this model is
+B{far} better.
+
+XML namespaces in suds are represented using a (2) element tuple
+containing the prefix and the URI.  Eg: I{('tns', 'http://myns')}
+
+"""
+
+import suds
+from suds import *
+from suds.sax import *
+from suds.sax.attribute import Attribute
+from suds.sax.document import Document
+from suds.sax.element import Element
+from suds.sax.text import Text
+
+import sys
+from xml.sax import make_parser, InputSource, ContentHandler
+from xml.sax.handler import feature_external_ges
+
+
+class Handler(ContentHandler):
+    """ sax hanlder """
+
+    def __init__(self):
+        self.nodes = [Document()]
+
+    def startElement(self, name, attrs):
+        top = self.top()
+        node = Element(unicode(name))
+        for a in attrs.getNames():
+            n = unicode(a)
+            v = unicode(attrs.getValue(a))
+            attribute = Attribute(n,v)
+            if self.mapPrefix(node, attribute):
+                continue
+            node.append(attribute)
+        node.charbuffer = []
+        top.append(node)
+        self.push(node)
+
+    def mapPrefix(self, node, attribute):
+        skip = False
+        if attribute.name == 'xmlns':
+            if len(attribute.value):
+                node.expns = unicode(attribute.value)
+            skip = True
+        elif attribute.prefix == 'xmlns':
+            prefix = attribute.name
+            node.nsprefixes[prefix] = unicode(attribute.value)
+            skip = True
+        return skip
+
+    def endElement(self, name):
+        name = unicode(name)
+        current = self.top()
+        if len(current.charbuffer):
+            current.text = Text(u''.join(current.charbuffer))
+        del current.charbuffer
+        if len(current):
+            current.trim()
+        if name == current.qname():
+            self.pop()
+        else:
+            raise Exception('malformed document')
+
+    def characters(self, content):
+        text = unicode(content)
+        node = self.top()
+        node.charbuffer.append(text)
+
+    def push(self, node):
+        self.nodes.append(node)
+        return node
+
+    def pop(self):
+        return self.nodes.pop()
+
+    def top(self):
+        return self.nodes[len(self.nodes)-1]
+
+
+class Parser:
+    """ SAX Parser """
+
+    @classmethod
+    def saxparser(cls):
+        p = make_parser()
+        p.setFeature(feature_external_ges, 0)
+        h = Handler()
+        p.setContentHandler(h)
+        return (p, h)
+
+    def parse(self, file=None, string=None):
+        """
+        SAX parse XML text.
+        @param file: Parse a python I{file-like} object.
+        @type file: I{file-like} object.
+        @param string: Parse string XML.
+        @type string: str
+        """
+        timer = suds.metrics.Timer()
+        timer.start()
+        sax, handler = self.saxparser()
+        if file is not None:
+            sax.parse(file)
+            timer.stop()
+            suds.metrics.log.debug('sax (%s) duration: %s', file, timer)
+            return handler.nodes[0]
+        if string is not None:
+            source = InputSource(None)
+            source.setByteStream(suds.BytesIO(string))
+            sax.parse(source)
+            timer.stop()
+            suds.metrics.log.debug('%s\nsax duration: %s', string, timer)
+            return handler.nodes[0]
diff --git a/suds/sax/text.py b/suds/sax/text.py
new file mode 100644
index 00000000..985386e4
--- /dev/null
+++ b/suds/sax/text.py
@@ -0,0 +1,116 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Contains XML text classes.
+"""
+
+from suds import *
+from suds.sax import *
+
+
+class Text(unicode):
+    """
+    An XML text object used to represent text content.
+    @ivar lang: The (optional) language flag.
+    @type lang: bool
+    @ivar escaped: The (optional) XML special character escaped flag.
+    @type escaped: bool
+    """
+    __slots__ = ('lang', 'escaped')
+
+    @classmethod
+    def __valid(cls, *args):
+        return len(args) and args[0] is not None
+
+    def __new__(cls, *args, **kwargs):
+        if cls.__valid(*args):
+            lang = kwargs.pop('lang', None)
+            escaped = kwargs.pop('escaped', False)
+            result = super(Text, cls).__new__(cls, *args, **kwargs)
+            result.lang = lang
+            result.escaped = escaped
+        else:
+            result = None
+        return result
+
+    def escape(self):
+        """
+        Encode (escape) special XML characters.
+        @return: The text with XML special characters escaped.
+        @rtype: L{Text}
+        """
+        if not self.escaped:
+            post = sax.encoder.encode(self)
+            escaped = ( post != self )
+            return Text(post, lang=self.lang, escaped=escaped)
+        return self
+
+    def unescape(self):
+        """
+        Decode (unescape) special XML characters.
+        @return: The text with escaped XML special characters decoded.
+        @rtype: L{Text}
+        """
+        if self.escaped:
+            post = sax.encoder.decode(self)
+            return Text(post, lang=self.lang)
+        return self
+
+    def trim(self):
+        post = self.strip()
+        return Text(post, lang=self.lang, escaped=self.escaped)
+
+    def __add__(self, other):
+        joined = u''.join((self, other))
+        result = Text(joined, lang=self.lang, escaped=self.escaped)
+        if isinstance(other, Text):
+            result.escaped = self.escaped or other.escaped
+        return result
+
+    def __repr__(self):
+        s = [self]
+        if self.lang is not None:
+            s.append(' [%s]' % self.lang)
+        if self.escaped:
+            s.append(' <escaped>')
+        return ''.join(s)
+
+    def __getstate__(self):
+        state = {}
+        for k in self.__slots__:
+            state[k] = getattr(self, k)
+        return state
+
+    def __setstate__(self, state):
+        for k in self.__slots__:
+            setattr(self, k, state[k])
+
+
+class Raw(Text):
+    """
+    Raw text which is not XML escaped.
+    This may include I{string} XML.
+    """
+    def escape(self):
+        return self
+
+    def unescape(self):
+        return self
+
+    def __add__(self, other):
+        joined = u''.join((self, other))
+        return Raw(joined, lang=self.lang)
diff --git a/suds/servicedefinition.py b/suds/servicedefinition.py
new file mode 100644
index 00000000..6b0e72f8
--- /dev/null
+++ b/suds/servicedefinition.py
@@ -0,0 +1,240 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{service definition} provides a textual representation of a service.
+"""
+
+from suds import *
+import suds.metrics as metrics
+from suds.sax import Namespace
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class ServiceDefinition(UnicodeMixin):
+    """
+    A service definition provides an object used to generate a textual description
+    of a service.
+    @ivar wsdl: A wsdl.
+    @type wsdl: L{wsdl.Definitions}
+    @ivar service: The service object.
+    @type service: L{suds.wsdl.Service}
+    @ivar ports: A list of port-tuple: (port, [(method-name, pdef)])
+    @type ports: [port-tuple,..]
+    @ivar prefixes: A list of remapped prefixes.
+    @type prefixes: [(prefix,uri),..]
+    @ivar types: A list of type definitions
+    @type types: [I{Type},..]
+    """
+
+    def __init__(self, wsdl, service):
+        """
+        @param wsdl: A WSDL object
+        @type wsdl: L{Definitions}
+        @param service: A service B{name}.
+        @type service: str
+        """
+        self.wsdl = wsdl
+        self.service = service
+        self.ports = []
+        self.params = []
+        self.types = []
+        self.prefixes = []
+        self.addports()
+        self.paramtypes()
+        self.publictypes()
+        self.getprefixes()
+        self.pushprefixes()
+
+    def pushprefixes(self):
+        """
+        Add our prefixes to the WSDL so that when users invoke methods
+        and reference the prefixes, they will resolve properly.
+        """
+        for ns in self.prefixes:
+            self.wsdl.root.addPrefix(ns[0], ns[1])
+
+    def addports(self):
+        """
+        Look through the list of service ports and construct a list of tuples
+        where each tuple is used to describe a port and its list of methods as:
+        (port, [method]).  Each method is a tuple: (name, [pdef,..]) where each
+        pdef is a tuple: (param-name, type).
+        """
+        timer = metrics.Timer()
+        timer.start()
+        for port in self.service.ports:
+            p = self.findport(port)
+            for op in port.binding.operations.values():
+                m = p[0].method(op.name)
+                binding = m.binding.input
+                method = (m.name, binding.param_defs(m))
+                p[1].append(method)
+                metrics.log.debug("method '%s' created: %s", m.name, timer)
+            p[1].sort()
+        timer.stop()
+
+    def findport(self, port):
+        """
+        Find and return a port tuple for the specified port.
+        Created and added when not found.
+        @param port: A port.
+        @type port: I{service.Port}
+        @return: A port tuple.
+        @rtype: (port, [method])
+        """
+        for p in self.ports:
+            if p[0] == p: return p
+        p = (port, [])
+        self.ports.append(p)
+        return p
+
+    def getprefixes(self):
+        """Add prefixes for each namespace referenced by parameter types."""
+        namespaces = []
+        for l in (self.params, self.types):
+            for t,r in l:
+                ns = r.namespace()
+                if ns[1] is None: continue
+                if ns[1] in namespaces: continue
+                if Namespace.xs(ns) or Namespace.xsd(ns):
+                    continue
+                namespaces.append(ns[1])
+                if t == r: continue
+                ns = t.namespace()
+                if ns[1] is None: continue
+                if ns[1] in namespaces: continue
+                namespaces.append(ns[1])
+        i = 0
+        namespaces.sort()
+        for u in namespaces:
+            p = self.nextprefix()
+            ns = (p, u)
+            self.prefixes.append(ns)
+
+    def paramtypes(self):
+        """Get all parameter types."""
+        for m in [p[1] for p in self.ports]:
+            for p in [p[1] for p in m]:
+                for pd in p:
+                    if pd[1] in self.params: continue
+                    item = (pd[1], pd[1].resolve())
+                    self.params.append(item)
+
+    def publictypes(self):
+        """Get all public types."""
+        for t in self.wsdl.schema.types.values():
+            if t in self.params: continue
+            if t in self.types: continue
+            item = (t, t)
+            self.types.append(item)
+        self.types.sort(key=lambda x: x[0].name)
+
+    def nextprefix(self):
+        """
+        Get the next available prefix.  This means a prefix starting with 'ns' with
+        a number appended as (ns0, ns1, ..) that is not already defined in the
+        WSDL document.
+        """
+        used = [ns[0] for ns in self.prefixes]
+        used += [ns[0] for ns in self.wsdl.root.nsprefixes.items()]
+        for n in range(0,1024):
+            p = 'ns%d'%n
+            if p not in used:
+                return p
+        raise Exception('prefixes exhausted')
+
+    def getprefix(self, u):
+        """
+        Get the prefix for the specified namespace (URI)
+        @param u: A namespace URI.
+        @type u: str
+        @return: The namspace.
+        @rtype: (prefix, uri).
+        """
+        for ns in Namespace.all:
+            if u == ns[1]: return ns[0]
+        for ns in self.prefixes:
+            if u == ns[1]: return ns[0]
+        raise Exception('ns (%s) not mapped'  % u)
+
+    def xlate(self, type):
+        """
+        Get a (namespace) translated I{qualified} name for specified type.
+        @param type: A schema type.
+        @type type: I{suds.xsd.sxbasic.SchemaObject}
+        @return: A translated I{qualified} name.
+        @rtype: str
+        """
+        resolved = type.resolve()
+        name = resolved.name
+        if type.multi_occurrence():
+            name += '[]'
+        ns = resolved.namespace()
+        if ns[1] == self.wsdl.tns[1]:
+            return name
+        prefix = self.getprefix(ns[1])
+        return ':'.join((prefix, name))
+
+    def description(self):
+        """
+        Get a textual description of the service for which this object represents.
+        @return: A textual description.
+        @rtype: str
+        """
+        s = []
+        indent = (lambda n :  '\n%*s'%(n*3,' '))
+        s.append('Service ( %s ) tns="%s"' % (self.service.name, self.wsdl.tns[1]))
+        s.append(indent(1))
+        s.append('Prefixes (%d)' % len(self.prefixes))
+        for p in self.prefixes:
+            s.append(indent(2))
+            s.append('%s = "%s"' % p)
+        s.append(indent(1))
+        s.append('Ports (%d):' % len(self.ports))
+        for p in self.ports:
+            s.append(indent(2))
+            s.append('(%s)' % p[0].name)
+            s.append(indent(3))
+            s.append('Methods (%d):' % len(p[1]))
+            for m in p[1]:
+                sig = []
+                s.append(indent(4))
+                sig.append(m[0])
+                sig.append('(')
+                sig.append(', '.join("%s %s" % (self.xlate(p[1]), p[0]) for p
+                    in m[1]))
+                sig.append(')')
+                try:
+                    s.append(''.join(sig))
+                except:
+                    pass
+            s.append(indent(3))
+            s.append('Types (%d):' % len(self.types))
+            for t in self.types:
+                s.append(indent(4))
+                s.append(self.xlate(t[0]))
+        s.append('\n\n')
+        return ''.join(s)
+
+    def __unicode__(self):
+        try:
+            return self.description()
+        except Exception, e:
+            log.exception(e)
+        return tostr(e)
diff --git a/suds/serviceproxy.py b/suds/serviceproxy.py
new file mode 100644
index 00000000..278c1896
--- /dev/null
+++ b/suds/serviceproxy.py
@@ -0,0 +1,80 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The service proxy provides access to web services.
+
+Replaced by: L{client.Client}
+"""
+
+from suds import *
+from suds.client import Client
+
+
+class ServiceProxy(UnicodeMixin):
+
+    """
+    A lightweight soap based web service proxy.
+    @ivar __client__: A client.
+        Everything is delegated to the 2nd generation API.
+    @type __client__: L{Client}
+    @note:  Deprecated, replaced by L{Client}.
+    """
+
+    def __init__(self, url, **kwargs):
+        """
+        @param url: The URL for the WSDL.
+        @type url: str
+        @param kwargs: keyword arguments.
+        @keyword faults: Raise faults raised by server (default:True),
+                else return tuple from service method invocation as (http code, object).
+        @type faults: boolean
+        @keyword proxy: An http proxy to be specified on requests (default:{}).
+                           The proxy is defined as {protocol:proxy,}
+        @type proxy: dict
+        """
+        client = Client(url, **kwargs)
+        self.__client__ = client
+
+    def get_instance(self, name):
+        """
+        Get an instance of a WSDL type by name
+        @param name: The name of a type defined in the WSDL.
+        @type name: str
+        @return: An instance on success, else None
+        @rtype: L{sudsobject.Object}
+        """
+        return self.__client__.factory.create(name)
+
+    def get_enum(self, name):
+        """
+        Get an instance of an enumeration defined in the WSDL by name.
+        @param name: The name of a enumeration defined in the WSDL.
+        @type name: str
+        @return: An instance on success, else None
+        @rtype: L{sudsobject.Object}
+        """
+        return self.__client__.factory.create(name)
+
+    def __unicode__(self):
+        return unicode(self.__client__)
+
+    def __getattr__(self, name):
+        builtin = name.startswith('__') and name.endswith('__')
+        if builtin:
+            return self.__dict__[name]
+        else:
+            return getattr(self.__client__.service, name)
diff --git a/suds/soaparray.py b/suds/soaparray.py
new file mode 100644
index 00000000..ea04fa7a
--- /dev/null
+++ b/suds/soaparray.py
@@ -0,0 +1,71 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{soaparray} module provides XSD extensions for handling
+soap (section 5) encoded arrays.
+"""
+
+from suds import *
+from logging import getLogger
+from suds.xsd.sxbasic import Factory as SXFactory
+from suds.xsd.sxbasic import Attribute as SXAttribute
+
+
+class Attribute(SXAttribute):
+    """
+    Represents an XSD <attribute/> that handles special
+    attributes that are extensions for WSDLs.
+    @ivar aty: Array type information.
+    @type aty: The value of wsdl:arrayType.
+    """
+
+    def __init__(self, schema, root, aty):
+        """
+        @param aty: Array type information.
+        @type aty: The value of wsdl:arrayType.
+        """
+        SXAttribute.__init__(self, schema, root)
+        if aty.endswith('[]'):
+            self.aty = aty[:-2]
+        else:
+            self.aty = aty
+
+    def autoqualified(self):
+        aqs = SXAttribute.autoqualified(self)
+        aqs.append('aty')
+        return aqs
+
+    def description(self):
+        d = SXAttribute.description(self)
+        d = d+('aty',)
+        return d
+
+#
+# Builder function, only builds Attribute when arrayType
+# attribute is defined on root.
+#
+def __fn(x, y):
+    ns = (None, "http://schemas.xmlsoap.org/wsdl/")
+    aty = y.get('arrayType', ns=ns)
+    if aty is None:
+        return SXAttribute(x, y)
+    return Attribute(x, y, aty)
+
+#
+# Remap <xs:attribute/> tags to __fn() builder.
+#
+SXFactory.maptag('attribute', __fn)
diff --git a/suds/store.py b/suds/store.py
new file mode 100644
index 00000000..e5931aa5
--- /dev/null
+++ b/suds/store.py
@@ -0,0 +1,596 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Support for holding XML document texts that may then be accessed internally by
+suds without having to download them from an external source. Also contains XML
+document content to be distributed alongside the suds library.
+
+"""
+
+import suds
+
+
+soap5_encoding_schema = suds.byte_str("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+    xmlns:tns="http://schemas.xmlsoap.org/soap/encoding/"
+    targetNamespace="http://schemas.xmlsoap.org/soap/encoding/">
+
+ <xs:attribute name="root">
+   <xs:annotation>
+     <xs:documentation>
+       'root' can be used to distinguish serialization roots from other
+       elements that are present in a serialization but are not roots of
+       a serialized value graph
+     </xs:documentation>
+   </xs:annotation>
+   <xs:simpleType>
+     <xs:restriction base="xs:boolean">
+       <xs:pattern value="0|1"/>
+     </xs:restriction>
+   </xs:simpleType>
+ </xs:attribute>
+
+  <xs:attributeGroup name="commonAttributes">
+    <xs:annotation>
+      <xs:documentation>
+        Attributes common to all elements that function as accessors or
+        represent independent (multi-ref) values.  The href attribute is
+        intended to be used in a manner like CONREF.  That is, the element
+        content should be empty iff the href attribute appears
+      </xs:documentation>
+    </xs:annotation>
+    <xs:attribute name="id" type="xs:ID"/>
+    <xs:attribute name="href" type="xs:anyURI"/>
+    <xs:anyAttribute namespace="##other" processContents="lax"/>
+  </xs:attributeGroup>
+
+  <!-- Global Attributes.  The following attributes are intended to be usable via qualified attribute names on any complex type referencing them. -->
+
+  <!-- Array attributes. Needed to give the type and dimensions of an array's contents, and the offset for partially-transmitted arrays. -->
+
+  <xs:simpleType name="arrayCoordinate">
+    <xs:restriction base="xs:string"/>
+  </xs:simpleType>
+
+  <xs:attribute name="arrayType" type="xs:string"/>
+  <xs:attribute name="offset" type="tns:arrayCoordinate"/>
+
+  <xs:attributeGroup name="arrayAttributes">
+    <xs:attribute ref="tns:arrayType"/>
+    <xs:attribute ref="tns:offset"/>
+  </xs:attributeGroup>
+
+  <xs:attribute name="position" type="tns:arrayCoordinate"/>
+
+  <xs:attributeGroup name="arrayMemberAttributes">
+    <xs:attribute ref="tns:position"/>
+  </xs:attributeGroup>
+
+  <xs:group name="Array">
+    <xs:sequence>
+      <xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
+    </xs:sequence>
+  </xs:group>
+
+  <xs:element name="Array" type="tns:Array"/>
+  <xs:complexType name="Array">
+    <xs:annotation>
+      <xs:documentation>
+       'Array' is a complex type for accessors identified by position
+      </xs:documentation>
+    </xs:annotation>
+    <xs:group ref="tns:Array" minOccurs="0"/>
+    <xs:attributeGroup ref="tns:arrayAttributes"/>
+    <xs:attributeGroup ref="tns:commonAttributes"/>
+  </xs:complexType>
+
+  <!-- 'Struct' is a complex type for accessors identified by name.
+       Constraint: No element may be have the same name as any other,
+       nor may any element have a maxOccurs > 1. -->
+
+  <xs:element name="Struct" type="tns:Struct"/>
+
+  <xs:group name="Struct">
+    <xs:sequence>
+      <xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
+    </xs:sequence>
+  </xs:group>
+
+  <xs:complexType name="Struct">
+    <xs:group ref="tns:Struct" minOccurs="0"/>
+    <xs:attributeGroup ref="tns:commonAttributes"/>
+  </xs:complexType>
+
+  <!-- 'Base64' can be used to serialize binary data using base64 encoding
+       as defined in RFC2045 but without the MIME line length limitation. -->
+
+  <xs:simpleType name="base64">
+    <xs:restriction base="xs:base64Binary"/>
+  </xs:simpleType>
+
+ <!-- Element declarations corresponding to each of the simple types in the
+      XML Schemas Specification. -->
+
+  <xs:element name="duration" type="tns:duration"/>
+  <xs:complexType name="duration">
+    <xs:simpleContent>
+      <xs:extension base="xs:duration">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="dateTime" type="tns:dateTime"/>
+  <xs:complexType name="dateTime">
+    <xs:simpleContent>
+      <xs:extension base="xs:dateTime">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="NOTATION" type="tns:NOTATION"/>
+  <xs:complexType name="NOTATION">
+    <xs:simpleContent>
+      <xs:extension base="xs:QName">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="time" type="tns:time"/>
+  <xs:complexType name="time">
+    <xs:simpleContent>
+      <xs:extension base="xs:time">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="date" type="tns:date"/>
+  <xs:complexType name="date">
+    <xs:simpleContent>
+      <xs:extension base="xs:date">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="gYearMonth" type="tns:gYearMonth"/>
+  <xs:complexType name="gYearMonth">
+    <xs:simpleContent>
+      <xs:extension base="xs:gYearMonth">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="gYear" type="tns:gYear"/>
+  <xs:complexType name="gYear">
+    <xs:simpleContent>
+      <xs:extension base="xs:gYear">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="gMonthDay" type="tns:gMonthDay"/>
+  <xs:complexType name="gMonthDay">
+    <xs:simpleContent>
+      <xs:extension base="xs:gMonthDay">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="gDay" type="tns:gDay"/>
+  <xs:complexType name="gDay">
+    <xs:simpleContent>
+      <xs:extension base="xs:gDay">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="gMonth" type="tns:gMonth"/>
+  <xs:complexType name="gMonth">
+    <xs:simpleContent>
+      <xs:extension base="xs:gMonth">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="boolean" type="tns:boolean"/>
+  <xs:complexType name="boolean">
+    <xs:simpleContent>
+      <xs:extension base="xs:boolean">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="base64Binary" type="tns:base64Binary"/>
+  <xs:complexType name="base64Binary">
+    <xs:simpleContent>
+      <xs:extension base="xs:base64Binary">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="hexBinary" type="tns:hexBinary"/>
+  <xs:complexType name="hexBinary">
+    <xs:simpleContent>
+     <xs:extension base="xs:hexBinary">
+       <xs:attributeGroup ref="tns:commonAttributes"/>
+     </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="float" type="tns:float"/>
+  <xs:complexType name="float">
+    <xs:simpleContent>
+      <xs:extension base="xs:float">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="double" type="tns:double"/>
+  <xs:complexType name="double">
+    <xs:simpleContent>
+      <xs:extension base="xs:double">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="anyURI" type="tns:anyURI"/>
+  <xs:complexType name="anyURI">
+    <xs:simpleContent>
+      <xs:extension base="xs:anyURI">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="QName" type="tns:QName"/>
+  <xs:complexType name="QName">
+    <xs:simpleContent>
+      <xs:extension base="xs:QName">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="string" type="tns:string"/>
+  <xs:complexType name="string">
+    <xs:simpleContent>
+      <xs:extension base="xs:string">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="normalizedString" type="tns:normalizedString"/>
+  <xs:complexType name="normalizedString">
+    <xs:simpleContent>
+      <xs:extension base="xs:normalizedString">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="token" type="tns:token"/>
+  <xs:complexType name="token">
+    <xs:simpleContent>
+      <xs:extension base="xs:token">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="language" type="tns:language"/>
+  <xs:complexType name="language">
+    <xs:simpleContent>
+      <xs:extension base="xs:language">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="Name" type="tns:Name"/>
+  <xs:complexType name="Name">
+    <xs:simpleContent>
+      <xs:extension base="xs:Name">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="NMTOKEN" type="tns:NMTOKEN"/>
+  <xs:complexType name="NMTOKEN">
+    <xs:simpleContent>
+      <xs:extension base="xs:NMTOKEN">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="NCName" type="tns:NCName"/>
+  <xs:complexType name="NCName">
+    <xs:simpleContent>
+      <xs:extension base="xs:NCName">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="NMTOKENS" type="tns:NMTOKENS"/>
+  <xs:complexType name="NMTOKENS">
+    <xs:simpleContent>
+      <xs:extension base="xs:NMTOKENS">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="ID" type="tns:ID"/>
+  <xs:complexType name="ID">
+    <xs:simpleContent>
+      <xs:extension base="xs:ID">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="IDREF" type="tns:IDREF"/>
+  <xs:complexType name="IDREF">
+    <xs:simpleContent>
+      <xs:extension base="xs:IDREF">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="ENTITY" type="tns:ENTITY"/>
+  <xs:complexType name="ENTITY">
+    <xs:simpleContent>
+      <xs:extension base="xs:ENTITY">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="IDREFS" type="tns:IDREFS"/>
+  <xs:complexType name="IDREFS">
+    <xs:simpleContent>
+      <xs:extension base="xs:IDREFS">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="ENTITIES" type="tns:ENTITIES"/>
+  <xs:complexType name="ENTITIES">
+    <xs:simpleContent>
+      <xs:extension base="xs:ENTITIES">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="decimal" type="tns:decimal"/>
+  <xs:complexType name="decimal">
+    <xs:simpleContent>
+      <xs:extension base="xs:decimal">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="integer" type="tns:integer"/>
+  <xs:complexType name="integer">
+    <xs:simpleContent>
+      <xs:extension base="xs:integer">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="nonPositiveInteger" type="tns:nonPositiveInteger"/>
+  <xs:complexType name="nonPositiveInteger">
+    <xs:simpleContent>
+      <xs:extension base="xs:nonPositiveInteger">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="negativeInteger" type="tns:negativeInteger"/>
+  <xs:complexType name="negativeInteger">
+    <xs:simpleContent>
+      <xs:extension base="xs:negativeInteger">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="long" type="tns:long"/>
+  <xs:complexType name="long">
+    <xs:simpleContent>
+      <xs:extension base="xs:long">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="int" type="tns:int"/>
+  <xs:complexType name="int">
+    <xs:simpleContent>
+      <xs:extension base="xs:int">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="short" type="tns:short"/>
+  <xs:complexType name="short">
+    <xs:simpleContent>
+      <xs:extension base="xs:short">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="byte" type="tns:byte"/>
+  <xs:complexType name="byte">
+    <xs:simpleContent>
+      <xs:extension base="xs:byte">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="nonNegativeInteger" type="tns:nonNegativeInteger"/>
+  <xs:complexType name="nonNegativeInteger">
+    <xs:simpleContent>
+      <xs:extension base="xs:nonNegativeInteger">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="unsignedLong" type="tns:unsignedLong"/>
+  <xs:complexType name="unsignedLong">
+    <xs:simpleContent>
+      <xs:extension base="xs:unsignedLong">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="unsignedInt" type="tns:unsignedInt"/>
+  <xs:complexType name="unsignedInt">
+    <xs:simpleContent>
+      <xs:extension base="xs:unsignedInt">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="unsignedShort" type="tns:unsignedShort"/>
+  <xs:complexType name="unsignedShort">
+    <xs:simpleContent>
+      <xs:extension base="xs:unsignedShort">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="unsignedByte" type="tns:unsignedByte"/>
+  <xs:complexType name="unsignedByte">
+    <xs:simpleContent>
+      <xs:extension base="xs:unsignedByte">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="positiveInteger" type="tns:positiveInteger"/>
+  <xs:complexType name="positiveInteger">
+    <xs:simpleContent>
+      <xs:extension base="xs:positiveInteger">
+        <xs:attributeGroup ref="tns:commonAttributes"/>
+      </xs:extension>
+    </xs:simpleContent>
+  </xs:complexType>
+
+  <xs:element name="anyType"/>
+</xs:schema>
+""")
+
+
+class DocumentStore:
+    """
+    The I{suds} document store provides a local repository for XML documents.
+
+    @cvar protocol: The URL protocol for the store.
+    @type protocol: str
+    @cvar store: The mapping of URL location to documents.
+    @type store: dict
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.__store = {
+            'schemas.xmlsoap.org/soap/encoding/':soap5_encoding_schema}
+        self.update = self.__store.update
+        self.update(*args, **kwargs)
+
+    def __len__(self):
+        # Implementation note:
+        #   We can not implement '__len__' as simply self.__store.__len__, as
+        # we do for 'update' because that causes py2to3 conversion to fail.
+        #                                            (08.05.2013.) (Jurko)
+        return len(self.__store)
+
+    def open(self, url):
+        """
+        Open a document at the specified URL.
+
+        Missing documents referenced using the internal 'suds' protocol are
+        reported by raising an exception. For other protocols, None is returned
+        instead.
+
+        @param url: A document URL.
+        @type url: str
+        @return: Document content or None if not found.
+        @rtype: bytes
+        """
+        protocol, location = self.__split(url)
+        content = self.__find(location)
+        if protocol == 'suds' and content is None:
+            raise Exception, 'location "%s" not in document store' % location
+        return content
+
+    def __find(self, location):
+        """
+        Find the specified location in the store.
+        @param location: The I{location} part of a URL.
+        @type location: str
+        @return: Document content or None if not found.
+        @rtype: bytes
+        """
+        return self.__store.get(location)
+
+    def __split(self, url):
+        """
+        Split the URL into I{protocol} and I{location}
+        @param url: A URL.
+        @param url: str
+        @return: (I{url}, I{location})
+        @rtype: tuple
+        """
+        parts = url.split('://', 1)
+        if len(parts) == 2:
+            return parts
+        return None, url
+
+
+defaultDocumentStore = DocumentStore()
diff --git a/suds/sudsobject.py b/suds/sudsobject.py
new file mode 100644
index 00000000..0c18d5a7
--- /dev/null
+++ b/suds/sudsobject.py
@@ -0,0 +1,391 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides a collection of suds objects primarily used for highly dynamic
+interactions with WSDL/XSD defined types.
+
+"""
+
+from suds import *
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+def items(sobject):
+    """
+    Extract the I{items} from a suds object.
+
+    Much like the items() method works on I{dict}.
+
+    @param sobject: A suds object
+    @type sobject: L{Object}
+    @return: A list of items contained in I{sobject}.
+    @rtype: [(key, value),...]
+
+    """
+    for item in sobject:
+        yield item
+
+
+def asdict(sobject):
+    """
+    Convert a sudsobject into a dictionary.
+
+    @param sobject: A suds object
+    @type sobject: L{Object}
+    @return: A python dictionary containing the items contained in I{sobject}.
+    @rtype: dict
+
+    """
+    return dict(items(sobject))
+
+def merge(a, b):
+    """
+    Merge all attributes and metadata from I{a} to I{b}.
+
+    @param a: A I{source} object
+    @type a: L{Object}
+    @param b: A I{destination} object
+    @type b: L{Object}
+
+    """
+    for item in a:
+        setattr(b, item[0], item[1])
+        b.__metadata__ = b.__metadata__
+    return b
+
+def footprint(sobject):
+    """
+    Get the I{virtual footprint} of the object.
+
+    This is really a count of all the significant value attributes in the
+    branch.
+
+    @param sobject: A suds object.
+    @type sobject: L{Object}
+    @return: The branch footprint.
+    @rtype: int
+
+    """
+    n = 0
+    for a in sobject.__keylist__:
+        v = getattr(sobject, a)
+        if v is None:
+            continue
+        if isinstance(v, Object):
+            n += footprint(v)
+            continue
+        if hasattr(v, "__len__"):
+            if len(v):
+                n += 1
+            continue
+        n += 1
+    return n
+
+
+class Factory:
+
+    cache = {}
+
+    @classmethod
+    def subclass(cls, name, bases, dict={}):
+        if not isinstance(bases, tuple):
+            bases = (bases,)
+        # name is of type unicode in python 2 -> not accepted by type()
+        name = str(name)
+        key = ".".join((name, str(bases)))
+        subclass = cls.cache.get(key)
+        if subclass is None:
+            subclass = type(name, bases, dict)
+            cls.cache[key] = subclass
+        return subclass
+
+    @classmethod
+    def object(cls, classname=None, dict={}):
+        if classname is not None:
+            subclass = cls.subclass(classname, Object)
+            inst = subclass()
+        else:
+            inst = Object()
+        for a in dict.items():
+            setattr(inst, a[0], a[1])
+        return inst
+
+    @classmethod
+    def metadata(cls):
+        return Metadata()
+
+    @classmethod
+    def property(cls, name, value=None):
+        subclass = cls.subclass(name, Property)
+        return subclass(value)
+
+
+class Object(UnicodeMixin):
+
+    def __init__(self):
+        self.__keylist__ = []
+        self.__printer__ = Printer()
+        self.__metadata__ = Metadata()
+
+    def __setattr__(self, name, value):
+        builtin = name.startswith("__") and name.endswith("__")
+        if not builtin and name not in self.__keylist__:
+            self.__keylist__.append(name)
+        self.__dict__[name] = value
+
+    def __delattr__(self, name):
+        try:
+            del self.__dict__[name]
+            builtin = name.startswith("__") and name.endswith("__")
+            if not builtin:
+                self.__keylist__.remove(name)
+        except Exception:
+            cls = self.__class__.__name__
+            raise AttributeError, "%s has no attribute '%s'" % (cls, name)
+
+    def __getitem__(self, name):
+        if isinstance(name, int):
+            name = self.__keylist__[int(name)]
+        return getattr(self, name)
+
+    def __setitem__(self, name, value):
+        setattr(self, name, value)
+
+    def __iter__(self):
+        return Iter(self)
+
+    def __len__(self):
+        return len(self.__keylist__)
+
+    def __contains__(self, name):
+        return name in self.__keylist__
+
+    def __repr__(self):
+        return str(self)
+
+    def __unicode__(self):
+        return self.__printer__.tostr(self)
+
+
+class Iter:
+
+    def __init__(self, sobject):
+        self.sobject = sobject
+        self.keylist = self.__keylist(sobject)
+        self.index = 0
+
+    def next(self):
+        keylist = self.keylist
+        nkeys = len(self.keylist)
+        while self.index < nkeys:
+            k = keylist[self.index]
+            self.index += 1
+            if hasattr(self.sobject, k):
+                v = getattr(self.sobject, k)
+                return (k, v)
+        raise StopIteration()
+
+    def __keylist(self, sobject):
+        keylist = sobject.__keylist__
+        try:
+            keyset = set(keylist)
+            ordering = sobject.__metadata__.ordering
+            ordered = set(ordering)
+            if not ordered.issuperset(keyset):
+                log.debug("%s must be superset of %s, ordering ignored",
+                    keylist, ordering)
+                raise KeyError()
+            return ordering
+        except Exception:
+            return keylist
+
+    def __iter__(self):
+        return self
+
+
+class Metadata(Object):
+    def __init__(self):
+        self.__keylist__ = []
+        self.__printer__ = Printer()
+
+
+class Facade(Object):
+    def __init__(self, name):
+        Object.__init__(self)
+        md = self.__metadata__
+        md.facade = name
+
+
+class Property(Object):
+
+    def __init__(self, value):
+        Object.__init__(self)
+        self.value = value
+
+    def items(self):
+        for item in self:
+            if item[0] != "value":
+                yield item
+
+    def get(self):
+        return self.value
+
+    def set(self, value):
+        self.value = value
+        return self
+
+
+class Printer:
+    """Pretty printing of a Object object."""
+
+    @classmethod
+    def indent(cls, n):
+        return "%*s" % (n * 3, " ")
+
+    def tostr(self, object, indent=-2):
+        """Get s string representation of object."""
+        history = []
+        return self.process(object, history, indent)
+
+    def process(self, object, h, n=0, nl=False):
+        """Print object using the specified indent (n) and newline (nl)."""
+        if object is None:
+            return "None"
+        if isinstance(object, Object):
+            if len(object) == 0:
+                return "<empty>"
+            return self.print_object(object, h, n + 2, nl)
+        if isinstance(object, dict):
+            if len(object) == 0:
+                return "<empty>"
+            return self.print_dictionary(object, h, n + 2, nl)
+        if isinstance(object, (list, tuple)):
+            if len(object) == 0:
+                return "<empty>"
+            return self.print_collection(object, h, n + 2)
+        if isinstance(object, basestring):
+            return '"%s"' % (tostr(object),)
+        return "%s" % (tostr(object),)
+
+    def print_object(self, d, h, n, nl=False):
+        """Print complex using the specified indent (n) and newline (nl)."""
+        s = []
+        cls = d.__class__
+        if d in h:
+            s.append("(")
+            s.append(cls.__name__)
+            s.append(")")
+            s.append("...")
+            return "".join(s)
+        h.append(d)
+        if nl:
+            s.append("\n")
+            s.append(self.indent(n))
+        if cls != Object:
+            s.append("(")
+            if isinstance(d, Facade):
+                s.append(d.__metadata__.facade)
+            else:
+                s.append(cls.__name__)
+            s.append(")")
+        s.append("{")
+        for item in d:
+            if self.exclude(d, item):
+                continue
+            item = self.unwrap(d, item)
+            s.append("\n")
+            s.append(self.indent(n+1))
+            if isinstance(item[1], (list,tuple)):
+                s.append(item[0])
+                s.append("[]")
+            else:
+                s.append(item[0])
+            s.append(" = ")
+            s.append(self.process(item[1], h, n, True))
+        s.append("\n")
+        s.append(self.indent(n))
+        s.append("}")
+        h.pop()
+        return "".join(s)
+
+    def print_dictionary(self, d, h, n, nl=False):
+        """Print complex using the specified indent (n) and newline (nl)."""
+        if d in h:
+            return "{}..."
+        h.append(d)
+        s = []
+        if nl:
+            s.append("\n")
+            s.append(self.indent(n))
+        s.append("{")
+        for item in d.items():
+            s.append("\n")
+            s.append(self.indent(n+1))
+            if isinstance(item[1], (list,tuple)):
+                s.append(tostr(item[0]))
+                s.append("[]")
+            else:
+                s.append(tostr(item[0]))
+            s.append(" = ")
+            s.append(self.process(item[1], h, n, True))
+        s.append("\n")
+        s.append(self.indent(n))
+        s.append("}")
+        h.pop()
+        return "".join(s)
+
+    def print_collection(self, c, h, n):
+        """Print collection using the specified indent (n) and newline (nl)."""
+        if c in h:
+            return "[]..."
+        h.append(c)
+        s = []
+        for item in c:
+            s.append("\n")
+            s.append(self.indent(n))
+            s.append(self.process(item, h, n - 2))
+            s.append(",")
+        h.pop()
+        return "".join(s)
+
+    def unwrap(self, d, item):
+        """Translate (unwrap) using an optional wrapper function."""
+        try:
+            md = d.__metadata__
+            pmd = getattr(md, "__print__", None)
+            if pmd is None:
+                return item
+            wrappers = getattr(pmd, "wrappers", {})
+            fn = wrappers.get(item[0], lambda x: x)
+            return (item[0], fn(item[1]))
+        except Exception:
+            pass
+        return item
+
+    def exclude(self, d, item):
+        """Check metadata for excluded items."""
+        try:
+            md = d.__metadata__
+            pmd = getattr(md, "__print__", None)
+            if pmd is None:
+                return False
+            excludes = getattr(pmd, "excludes", [])
+            return item[0] in excludes
+        except Exception:
+            pass
+        return False
diff --git a/suds/transport/__init__.py b/suds/transport/__init__.py
new file mode 100644
index 00000000..b193aead
--- /dev/null
+++ b/suds/transport/__init__.py
@@ -0,0 +1,135 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Contains transport interface (classes).
+
+"""
+
+from suds import UnicodeMixin
+
+
+class TransportError(Exception):
+    def __init__(self, reason, httpcode, fp=None):
+        Exception.__init__(self, reason)
+        self.httpcode = httpcode
+        self.fp = fp
+
+
+class Request(UnicodeMixin):
+    """
+    A transport request.
+
+    @ivar url: The URL for the request.
+    @type url: str
+    @ivar message: The message to be sent in a POST request.
+    @type message: str
+    @ivar headers: The HTTP headers to be used for the request.
+    @type headers: dict
+
+    """
+
+    def __init__(self, url, message=None):
+        """
+        @param url: The URL for the request.
+        @type url: str
+        @param message: The (optional) message to be sent in the request.
+        @type message: str
+
+        """
+        self.url = url
+        self.headers = {}
+        self.message = message
+
+    def __unicode__(self):
+        return u"""\
+URL: %s
+HEADERS: %s
+MESSAGE:
+%s""" % (self.url, self.headers, self.message)
+
+
+class Reply(UnicodeMixin):
+    """
+    A transport reply.
+
+    @ivar code: The HTTP code returned.
+    @type code: int
+    @ivar message: The message to be sent in a POST request.
+    @type message: str
+    @ivar headers: The HTTP headers to be used for the request.
+    @type headers: dict
+
+    """
+
+    def __init__(self, code, headers, message):
+        """
+        @param code: The HTTP code returned.
+        @type code: int
+        @param headers: The HTTP returned headers.
+        @type headers: dict
+        @param message: The (optional) reply message received.
+        @type message: str
+
+        """
+        self.code = code
+        self.headers = headers
+        self.message = message
+
+    def __unicode__(self):
+        return u"""\
+CODE: %s
+HEADERS: %s
+MESSAGE:
+%s""" % (self.code, self.headers, self.message)
+
+
+class Transport:
+    """The transport I{interface}."""
+
+    def __init__(self):
+        from suds.transport.options import Options
+        self.options = Options()
+
+    def open(self, request):
+        """
+        Open the URL in the specified request.
+
+        @param request: A transport request.
+        @type request: L{Request}
+        @return: An input stream.
+        @rtype: stream
+        @raise TransportError: On all transport errors.
+
+        """
+        raise Exception('not-implemented')
+
+    def send(self, request):
+        """
+        Send soap message. Implementations are expected to handle:
+            - proxies
+            - I{HTTP} headers
+            - cookies
+            - sending message
+            - brokering exceptions into L{TransportError}
+
+        @param request: A transport request.
+        @type request: L{Request}
+        @return: The reply
+        @rtype: L{Reply}
+        @raise TransportError: On all transport errors.
+
+        """
+        raise Exception('not-implemented')
diff --git a/suds/transport/http.py b/suds/transport/http.py
new file mode 100644
index 00000000..e3d042be
--- /dev/null
+++ b/suds/transport/http.py
@@ -0,0 +1,254 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Contains classes for basic HTTP transport implementations.
+
+"""
+
+from suds.properties import Unskin
+from suds.transport import *
+
+import base64
+from cookielib import CookieJar
+import httplib
+import socket
+import sys
+import urllib2
+from urlparse import urlparse
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class HttpTransport(Transport):
+    """
+    Basic HTTP transport implemented using using urllib2, that provides for
+    cookies & proxies but no authentication.
+
+    """
+
+    def __init__(self, **kwargs):
+        """
+        @param kwargs: Keyword arguments.
+            - B{proxy} - An http proxy to be specified on requests.
+                 The proxy is defined as {protocol:proxy,}
+                    - type: I{dict}
+                    - default: {}
+            - B{timeout} - Set the url open timeout (seconds).
+                    - type: I{float}
+                    - default: 90
+
+        """
+        Transport.__init__(self)
+        Unskin(self.options).update(kwargs)
+        self.cookiejar = CookieJar()
+        self.proxy = {}
+        self.urlopener = None
+
+    def open(self, request):
+        try:
+            url = self.__get_request_url(request)
+            log.debug('opening (%s)', url)
+            u2request = urllib2.Request(url)
+            self.proxy = self.options.proxy
+            return self.u2open(u2request)
+        except urllib2.HTTPError, e:
+            raise TransportError(str(e), e.code, e.fp)
+
+    def send(self, request):
+        result = None
+        url = self.__get_request_url(request)
+        msg = request.message
+        headers = request.headers
+        try:
+            u2request = urllib2.Request(url, msg, headers)
+            self.addcookies(u2request)
+            self.proxy = self.options.proxy
+            request.headers.update(u2request.headers)
+            log.debug('sending:\n%s', request)
+            fp = self.u2open(u2request)
+            self.getcookies(fp, u2request)
+            if sys.version_info < (3, 0):
+                headers = fp.headers.dict
+            else:
+                headers = fp.headers
+            result = Reply(httplib.OK, headers, fp.read())
+            log.debug('received:\n%s', result)
+        except urllib2.HTTPError, e:
+            if e.code in (httplib.ACCEPTED, httplib.NO_CONTENT):
+                result = None
+            else:
+                raise TransportError(e.msg, e.code, e.fp)
+        return result
+
+    def addcookies(self, u2request):
+        """
+        Add cookies in the cookiejar to the request.
+
+        @param u2request: A urllib2 request.
+        @rtype: u2request: urllib2.Request.
+
+        """
+        self.cookiejar.add_cookie_header(u2request)
+
+    def getcookies(self, fp, u2request):
+        """
+        Add cookies in the request to the cookiejar.
+
+        @param u2request: A urllib2 request.
+        @rtype: u2request: urllib2.Request.
+
+        """
+        self.cookiejar.extract_cookies(fp, u2request)
+
+    def u2open(self, u2request):
+        """
+        Open a connection.
+
+        @param u2request: A urllib2 request.
+        @type u2request: urllib2.Request.
+        @return: The opened file-like urllib2 object.
+        @rtype: fp
+
+        """
+        tm = self.options.timeout
+        url = self.u2opener()
+        if (sys.version_info < (3, 0)) and (self.u2ver() < 2.6):
+            socket.setdefaulttimeout(tm)
+            return url.open(u2request)
+        return url.open(u2request, timeout=tm)
+
+    def u2opener(self):
+        """
+        Create a urllib opener.
+
+        @return: An opener.
+        @rtype: I{OpenerDirector}
+
+        """
+        if self.urlopener is None:
+            return urllib2.build_opener(*self.u2handlers())
+        return self.urlopener
+
+    def u2handlers(self):
+        """
+        Get a collection of urllib handlers.
+
+        @return: A list of handlers to be installed in the opener.
+        @rtype: [Handler,...]
+
+        """
+        handlers = []
+        handlers.append(urllib2.ProxyHandler(self.proxy))
+        return handlers
+
+    def u2ver(self):
+        """
+        Get the major/minor version of the urllib2 lib.
+
+        @return: The urllib2 version.
+        @rtype: float
+        """
+        try:
+            part = urllib2.__version__.split('.', 1)
+            return float('.'.join(part))
+        except Exception, e:
+            log.exception(e)
+            return 0
+
+    def __deepcopy__(self, memo={}):
+        clone = self.__class__()
+        p = Unskin(self.options)
+        cp = Unskin(clone.options)
+        cp.update(p)
+        return clone
+
+    @staticmethod
+    def __get_request_url(request):
+        """
+        Returns the given request's URL, properly encoded for use with urllib.
+
+        URLs are allowed to be:
+            under Python 2.x: unicode strings, single-byte strings;
+            under Python 3.x: unicode strings.
+        In any case, they are allowed to contain ASCII characters only. We
+        raise a UnicodeError derived exception if they contain any non-ASCII
+        characters (UnicodeEncodeError or UnicodeDecodeError depending on
+        whether the URL was specified as a unicode or a single-byte string).
+
+        Python 3.x httplib.client implementation must be given a unicode string
+        and not a bytes object and the given string is internally converted to
+        a bytes object using an explicitly specified ASCII encoding.
+
+        Python 2.7 httplib implementation expects the URL passed to it to not
+        be a unicode string. If it is, then passing it to the underlying
+        httplib Request object will cause that object to forcefully convert all
+        of its data to unicode, assuming that data contains ASCII data only and
+        raising a UnicodeDecodeError exception if it does not (caused by simple
+        unicode + string concatenation).
+
+        Python 2.4 httplib implementation does not really care about this as it
+        does not use the internal optimization present in the Python 2.7
+        implementation causing all the requested data to be converted to
+        unicode.
+
+        """
+        url = request.url
+        py2 = sys.version_info < (3, 0)
+        if py2 and isinstance(url, str):
+            encodedURL = url
+            decodedURL = url.decode("ascii")
+        else:
+            # On Python3, calling encode() on a bytes or a bytearray object
+            # raises an AttributeError exception.
+            assert py2 or not isinstance(url, bytes)
+            assert py2 or not isinstance(url, bytearray)
+            decodedURL = url
+            encodedURL = url.encode("ascii")
+        if py2:
+            return encodedURL  # Python 2 urllib - single-byte URL string.
+        return decodedURL  # Python 3 urllib - unicode URL string.
+
+
+class HttpAuthenticated(HttpTransport):
+    """
+    Provides basic HTTP authentication for servers that do not follow the
+    specified challenge/response model. Appends the I{Authorization} HTTP
+    header with base64 encoded credentials on every HTTP request.
+    """
+
+    def open(self, request):
+        self.addcredentials(request)
+        return HttpTransport.open(self, request)
+
+    def send(self, request):
+        self.addcredentials(request)
+        return HttpTransport.send(self, request)
+
+    def addcredentials(self, request):
+        credentials = self.credentials()
+        if not (None in credentials):
+            credentials = ':'.join(credentials)
+            if sys.version_info < (3,0):
+                basic = 'Basic %s' % base64.b64encode(credentials)
+            else:
+                encodedBytes = base64.urlsafe_b64encode(credentials.encode())
+                encodedString = encodedBytes.decode()
+                basic = 'Basic %s' % encodedString
+            request.headers['Authorization'] = basic
+
+    def credentials(self):
+        return self.options.username, self.options.password
diff --git a/suds/transport/https.py b/suds/transport/https.py
new file mode 100644
index 00000000..cdff55ab
--- /dev/null
+++ b/suds/transport/https.py
@@ -0,0 +1,99 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Contains classes for authenticated HTTP transport implementations.
+
+"""
+
+from suds.transport import *
+from suds.transport.http import HttpTransport
+
+import urllib2
+
+
+class HttpAuthenticated(HttpTransport):
+    """
+    Provides basic HTTP authentication that follows the RFC-2617 specification.
+
+    As defined by specifications, credentials are provided to the server upon
+    request (HTTP/1.0 401 Authorization Required) by the server only.
+
+    @ivar pm: The password manager.
+    @ivar handler: The authentication handler.
+
+    """
+
+    def __init__(self, **kwargs):
+        """
+        @param kwargs: Keyword arguments.
+            - B{proxy} - An HTTP proxy to be specified on requests.
+                 The proxy is defined as {protocol:proxy,}
+                    - type: I{dict}
+                    - default: {}
+            - B{timeout} - Set the URL open timeout (seconds).
+                    - type: I{float}
+                    - default: 90
+            - B{username} - The username used for HTTP authentication.
+                    - type: I{str}
+                    - default: None
+            - B{password} - The password used for HTTP authentication.
+                    - type: I{str}
+                    - default: None
+
+        """
+        HttpTransport.__init__(self, **kwargs)
+        self.pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
+
+    def open(self, request):
+        self.addcredentials(request)
+        return HttpTransport.open(self, request)
+
+    def send(self, request):
+        self.addcredentials(request)
+        return HttpTransport.send(self, request)
+
+    def addcredentials(self, request):
+        credentials = self.credentials()
+        if None not in credentials:
+            u = credentials[0]
+            p = credentials[1]
+            self.pm.add_password(None, request.url, u, p)
+
+    def credentials(self):
+        return self.options.username, self.options.password
+
+    def u2handlers(self):
+        handlers = HttpTransport.u2handlers(self)
+        handlers.append(urllib2.HTTPBasicAuthHandler(self.pm))
+        return handlers
+
+
+class WindowsHttpAuthenticated(HttpAuthenticated):
+    """
+    Provides Windows (NTLM) based HTTP authentication.
+
+    @author: Christopher Bess
+
+    """
+
+    def u2handlers(self):
+        try:
+            from ntlm import HTTPNtlmAuthHandler
+        except ImportError:
+            raise Exception("Cannot import python-ntlm module")
+        handlers = HttpTransport.u2handlers(self)
+        handlers.append(HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(self.pm))
+        return handlers
diff --git a/suds/transport/options.py b/suds/transport/options.py
new file mode 100644
index 00000000..f6a071e0
--- /dev/null
+++ b/suds/transport/options.py
@@ -0,0 +1,58 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, 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 MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Classes modeling transport options.
+
+"""
+
+
+from suds.transport import *
+from suds.properties import *
+
+
+class Options(Skin):
+    """
+    Options:
+        - B{proxy} - An HTTP proxy to be specified on requests, defined as
+            {protocol:proxy, ...}.
+                - type: I{dict}
+                - default: {}
+        - B{timeout} - Set the URL open timeout (seconds).
+                - type: I{float}
+                - default: 90
+        - B{headers} - Extra HTTP headers.
+                - type: I{dict}
+                    - I{str} B{http} - The I{HTTP} protocol proxy URL.
+                    - I{str} B{https} - The I{HTTPS} protocol proxy URL.
+                - default: {}
+        - B{username} - The username used for HTTP authentication.
+                - type: I{str}
+                - default: None
+        - B{password} - The password used for HTTP authentication.
+                - type: I{str}
+                - default: None
+
+    """
+
+    def __init__(self, **kwargs):
+        domain = __name__
+        definitions = [
+            Definition('proxy', dict, {}),
+            Definition('timeout', (int,float), 90),
+            Definition('headers', dict, {}),
+            Definition('username', basestring, None),
+            Definition('password', basestring, None)]
+        Skin.__init__(self, domain, definitions, kwargs)
diff --git a/suds/umx/__init__.py b/suds/umx/__init__.py
new file mode 100644
index 00000000..ca65cad1
--- /dev/null
+++ b/suds/umx/__init__.py
@@ -0,0 +1,56 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides modules containing classes to support
+unmarshalling (XML).
+"""
+
+from suds.sudsobject import Object
+
+
+
+class Content(Object):
+    """
+    @ivar node: The content source node.
+    @type node: L{sax.element.Element}
+    @ivar data: The (optional) content data.
+    @type data: L{Object}
+    @ivar text: The (optional) content (xml) text.
+    @type text: basestring
+    """
+
+    extensions = []
+
+    def __init__(self, node, **kwargs):
+        Object.__init__(self)
+        self.node = node
+        self.data = None
+        self.text = None
+        for k,v in kwargs.items():
+            setattr(self, k, v)
+
+    def __getattr__(self, name):
+        if name not in self.__dict__:
+            if name in self.extensions:
+                v = None
+                setattr(self, name, v)
+            else:
+                raise AttributeError, \
+                    'Content has no attribute %s' % name
+        else:
+            v = self.__dict__[name]
+        return v
diff --git a/suds/umx/attrlist.py b/suds/umx/attrlist.py
new file mode 100644
index 00000000..df8da0bf
--- /dev/null
+++ b/suds/umx/attrlist.py
@@ -0,0 +1,88 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides filtered attribute list classes.
+"""
+
+from suds import *
+from suds.umx import *
+from suds.sax import Namespace
+
+
+class AttrList:
+    """
+    A filtered attribute list.
+    Items are included during iteration if they are in either the (xs) or
+    (xml) namespaces.
+    @ivar raw: The I{raw} attribute list.
+    @type raw: list
+    """
+    def __init__(self, attributes):
+        """
+        @param attributes: A list of attributes
+        @type attributes: list
+        """
+        self.raw = attributes
+
+    def real(self):
+        """
+        Get list of I{real} attributes which exclude xs and xml attributes.
+        @return: A list of I{real} attributes.
+        @rtype: I{generator}
+        """
+        for a in self.raw:
+            if self.skip(a): continue
+            yield a
+
+    def rlen(self):
+        """
+        Get the number of I{real} attributes which exclude xs and xml attributes.
+        @return: A count of I{real} attributes.
+        @rtype: L{int}
+        """
+        n = 0
+        for a in self.real():
+            n += 1
+        return n
+
+    def lang(self):
+        """
+        Get list of I{filtered} attributes which exclude xs.
+        @return: A list of I{filtered} attributes.
+        @rtype: I{generator}
+        """
+        for a in self.raw:
+            if a.qname() == 'xml:lang':
+                return a.value
+            return None
+
+    def skip(self, attr):
+        """
+        Get whether to skip (filter-out) the specified attribute.
+        @param attr: An attribute.
+        @type attr: I{Attribute}
+        @return: True if should be skipped.
+        @rtype: bool
+        """
+        ns = attr.namespace()
+        skip = (
+            Namespace.xmlns[1],
+            'http://schemas.xmlsoap.org/soap/encoding/',
+            'http://schemas.xmlsoap.org/soap/envelope/',
+            'http://www.w3.org/2003/05/soap-envelope',
+        )
+        return ( Namespace.xs(ns) or ns[1] in skip )
diff --git a/suds/umx/basic.py b/suds/umx/basic.py
new file mode 100644
index 00000000..888a2122
--- /dev/null
+++ b/suds/umx/basic.py
@@ -0,0 +1,41 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides basic unmarshaller classes.
+"""
+
+from logging import getLogger
+from suds import *
+from suds.umx import *
+from suds.umx.core import Core
+
+
+class Basic(Core):
+    """
+    A object builder (unmarshaller).
+    """
+
+    def process(self, node):
+        """
+        Process an object graph representation of the xml I{node}.
+        @param node: An XML tree.
+        @type node: L{sax.element.Element}
+        @return: A suds object.
+        @rtype: L{Object}
+        """
+        content = Content(node)
+        return Core.process(self, content)
diff --git a/suds/umx/core.py b/suds/umx/core.py
new file mode 100644
index 00000000..4db8eaa7
--- /dev/null
+++ b/suds/umx/core.py
@@ -0,0 +1,214 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides base classes for XML->object I{unmarshalling}.
+"""
+
+from suds import *
+from suds.umx import *
+from suds.umx.attrlist import AttrList
+from suds.sax.text import Text
+from suds.sudsobject import Factory, merge
+
+
+reserved = {'class':'cls', 'def':'dfn'}
+
+
+class Core:
+    """
+    The abstract XML I{node} unmarshaller.  This class provides the
+    I{core} unmarshalling functionality.
+    """
+
+    def process(self, content):
+        """
+        Process an object graph representation of the xml I{node}.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: A suds object.
+        @rtype: L{Object}
+        """
+        self.reset()
+        return self.append(content)
+
+    def append(self, content):
+        """
+        Process the specified node and convert the XML document into
+        a I{suds} L{object}.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: A I{append-result} tuple as: (L{Object}, I{value})
+        @rtype: I{append-result}
+        @note: This is not the proper entry point.
+        @see: L{process()}
+        """
+        self.start(content)
+        self.append_attributes(content)
+        self.append_children(content)
+        self.append_text(content)
+        self.end(content)
+        return self.postprocess(content)
+
+    def postprocess(self, content):
+        """
+        Perform final processing of the resulting data structure as follows:
+          - Mixed values (children and text) will have a result of the I{content.node}.
+          - Simi-simple values (attributes, no-children and text) will have a result of a
+             property object.
+          - Simple values (no-attributes, no-children with text nodes) will have a string
+             result equal to the value of the content.node.getText().
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: The post-processed result.
+        @rtype: I{any}
+        """
+        node = content.node
+        if len(node.children) and node.hasText():
+            return node
+        attributes = AttrList(node.attributes)
+        if attributes.rlen() and \
+            not len(node.children) and \
+            node.hasText():
+                p = Factory.property(node.name, node.getText())
+                return merge(content.data, p)
+        if len(content.data):
+            return content.data
+        lang = attributes.lang()
+        if content.node.isnil():
+            return None
+        if not len(node.children) and content.text is None:
+            if self.nillable(content):
+                return None
+            else:
+                return Text('', lang=lang)
+        if isinstance(content.text, basestring):
+            return Text(content.text, lang=lang)
+        else:
+            return content.text
+
+    def append_attributes(self, content):
+        """
+        Append attribute nodes into L{Content.data}.
+        Attributes in the I{schema} or I{xml} namespaces are skipped.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        attributes = AttrList(content.node.attributes)
+        for attr in attributes.real():
+            name = attr.name
+            value = attr.value
+            self.append_attribute(name, value, content)
+
+    def append_attribute(self, name, value, content):
+        """
+        Append an attribute name/value into L{Content.data}.
+        @param name: The attribute name
+        @type name: basestring
+        @param value: The attribute's value
+        @type value: basestring
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        key = name
+        key = '_%s' % reserved.get(key, key)
+        setattr(content.data, key, value)
+
+    def append_children(self, content):
+        """
+        Append child nodes into L{Content.data}
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        for child in content.node:
+            cont = Content(child)
+            cval = self.append(cont)
+            key = reserved.get(child.name, child.name)
+            if key in content.data:
+                v = getattr(content.data, key)
+                if isinstance(v, list):
+                    v.append(cval)
+                else:
+                    setattr(content.data, key, [v, cval])
+                continue
+            if self.multi_occurrence(cont):
+                if cval is None:
+                    setattr(content.data, key, [])
+                else:
+                    setattr(content.data, key, [cval,])
+            else:
+                setattr(content.data, key, cval)
+
+    def append_text(self, content):
+        """
+        Append text nodes into L{Content.data}
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        if content.node.hasText():
+            content.text = content.node.getText()
+
+    def reset(self):
+        pass
+
+    def start(self, content):
+        """
+        Processing on I{node} has started.  Build and return
+        the proper object.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: A subclass of Object.
+        @rtype: L{Object}
+        """
+        content.data = Factory.object(content.node.name)
+
+    def end(self, content):
+        """
+        Processing on I{node} has ended.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        pass
+
+    def single_occurrence(self, content):
+        """
+        Get whether the content has at most a single occurrence (not a list).
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: True if content has at most a single occurrence, else False.
+        @rtype: boolean
+        '"""
+        return not self.multi_occurrence(content)
+
+    def multi_occurrence(self, content):
+        """
+        Get whether the content has more than one occurrence (a list).
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: True if content has more than one occurrence, else False.
+        @rtype: boolean
+        '"""
+        return False
+
+    def nillable(self, content):
+        """
+        Get whether the object is nillable.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: True if nillable, else False
+        @rtype: boolean
+        '"""
+        return False
diff --git a/suds/umx/encoded.py b/suds/umx/encoded.py
new file mode 100644
index 00000000..bb454e1c
--- /dev/null
+++ b/suds/umx/encoded.py
@@ -0,0 +1,126 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides soap encoded unmarshaller classes.
+"""
+
+from suds import *
+from suds.umx import *
+from suds.umx.typed import Typed
+from suds.sax import Namespace
+
+
+#
+# Add encoded extensions
+# aty = The soap (section 5) encoded array type.
+#
+Content.extensions.append('aty')
+
+
+class Encoded(Typed):
+    """
+    A SOAP section (5) encoding unmarshaller.
+    This marshaller supports rpc/encoded soap styles.
+    """
+
+    def start(self, content):
+        #
+        # Grab the array type and continue
+        #
+        self.setaty(content)
+        Typed.start(self, content)
+
+    def end(self, content):
+        #
+        # Squash soap encoded arrays into python lists.  This is
+        # also where we insure that empty arrays are represented
+        # as empty python lists.
+        #
+        aty = content.aty
+        if aty is not None:
+            self.promote(content)
+        return Typed.end(self, content)
+
+    def postprocess(self, content):
+        #
+        # Ensure proper rendering of empty arrays.
+        #
+        if content.aty is None:
+            return Typed.postprocess(self, content)
+        else:
+            return content.data
+
+    def setaty(self, content):
+        """
+        Grab the (aty) soap-enc:arrayType and attach it to the
+        content for proper array processing later in end().
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        @return: self
+        @rtype: L{Encoded}
+        """
+        name = 'arrayType'
+        ns = (None, 'http://schemas.xmlsoap.org/soap/encoding/')
+        aty = content.node.get(name, ns)
+        if aty is not None:
+            content.aty = aty
+            parts = aty.split('[')
+            ref = parts[0]
+            if len(parts) == 2:
+                self.applyaty(content, ref)
+            else:
+                pass # (2) dimensional array
+        return self
+
+    def applyaty(self, content, xty):
+        """
+        Apply the type referenced in the I{arrayType} to the content
+        (child nodes) of the array.  Each element (node) in the array
+        that does not have an explicit xsi:type attribute is given one
+        based on the I{arrayType}.
+        @param content: An array content.
+        @type content: L{Content}
+        @param xty: The XSI type reference.
+        @type xty: str
+        @return: self
+        @rtype: L{Encoded}
+        """
+        name = 'type'
+        ns = Namespace.xsins
+        parent = content.node
+        for child in parent.getChildren():
+            ref = child.get(name, ns)
+            if ref is None:
+                parent.addPrefix(ns[0], ns[1])
+                attr = ':'.join((ns[0], name))
+                child.set(attr, xty)
+        return self
+
+    def promote(self, content):
+        """
+        Promote (replace) the content.data with the first attribute
+        of the current content.data that is a I{list}.  Note: the
+        content.data may be empty or contain only _x attributes.
+        In either case, the content.data is assigned an empty list.
+        @param content: An array content.
+        @type content: L{Content}
+        """
+        for n,v in content.data:
+            if isinstance(v, list):
+                content.data = v
+                return
+        content.data = []
diff --git a/suds/umx/typed.py b/suds/umx/typed.py
new file mode 100644
index 00000000..7ab83298
--- /dev/null
+++ b/suds/umx/typed.py
@@ -0,0 +1,140 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+Provides typed unmarshaller classes.
+"""
+
+from suds import *
+from suds.umx import *
+from suds.umx.core import Core
+from suds.resolver import NodeResolver, Frame
+from suds.sudsobject import Factory
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+#
+# Add typed extensions
+# type = The expected xsd type
+# real = The 'true' XSD type
+#
+Content.extensions.append('type')
+Content.extensions.append('real')
+
+
+class Typed(Core):
+    """
+    A I{typed} XML unmarshaller
+    @ivar resolver: A schema type resolver.
+    @type resolver: L{NodeResolver}
+    """
+
+    def __init__(self, schema):
+        """
+        @param schema: A schema object.
+        @type schema: L{xsd.schema.Schema}
+        """
+        self.resolver = NodeResolver(schema)
+
+    def process(self, node, type):
+        """
+        Process an object graph representation of the xml L{node}.
+        @param node: An XML tree.
+        @type node: L{sax.element.Element}
+        @param type: The I{optional} schema type.
+        @type type: L{xsd.sxbase.SchemaObject}
+        @return: A suds object.
+        @rtype: L{Object}
+        """
+        content = Content(node)
+        content.type = type
+        return Core.process(self, content)
+
+    def reset(self):
+        log.debug('reset')
+        self.resolver.reset()
+
+    def start(self, content):
+        #
+        # Resolve to the schema type; build an object and setup metadata.
+        #
+        if content.type is None:
+            found = self.resolver.find(content.node)
+            if found is None:
+                log.error(self.resolver.schema)
+                raise TypeNotFound(content.node.qname())
+            content.type = found
+        else:
+            known = self.resolver.known(content.node)
+            frame = Frame(content.type, resolved=known)
+            self.resolver.push(frame)
+        real = self.resolver.top().resolved
+        content.real = real
+        cls_name = real.name
+        if cls_name is None:
+            cls_name = content.node.name
+        content.data = Factory.object(cls_name)
+        md = content.data.__metadata__
+        md.sxtype = real
+
+    def end(self, content):
+        self.resolver.pop()
+
+    def multi_occurrence(self, content):
+        return content.type.multi_occurrence()
+
+    def nillable(self, content):
+        resolved = content.type.resolve()
+        return ( content.type.nillable or \
+            (resolved.builtin() and resolved.nillable ) )
+
+    def append_attribute(self, name, value, content):
+        """
+        Append an attribute name/value into L{Content.data}.
+        @param name: The attribute name
+        @type name: basestring
+        @param value: The attribute's value
+        @type value: basestring
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        type = self.resolver.findattr(name)
+        if type is None:
+            log.warn('attribute (%s) type, not-found', name)
+        else:
+            value = self.translated(value, type)
+        Core.append_attribute(self, name, value, content)
+
+    def append_text(self, content):
+        """
+        Append text nodes into L{Content.data}
+        Here is where the I{true} type is used to translate the value
+        into the proper python type.
+        @param content: The current content being unmarshalled.
+        @type content: L{Content}
+        """
+        Core.append_text(self, content)
+        known = self.resolver.top().resolved
+        content.text = self.translated(content.text, known)
+
+    def translated(self, value, type):
+        """ translate using the schema type """
+        if value is not None:
+            resolved = type.resolve()
+            return resolved.translate(value)
+        return value
diff --git a/suds/version.py b/suds/version.py
new file mode 100644
index 00000000..28f5a90a
--- /dev/null
+++ b/suds/version.py
@@ -0,0 +1,26 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""
+Module containing the library's version information.
+
+  This version information has been extracted into a separate file so it can be
+read from the setup.py script without having to import the suds package itself.
+See the setup.py script for more detailed information.
+
+"""
+
+__version__ = "0.6"
+__build__ = ""
diff --git a/suds/wsdl.py b/suds/wsdl.py
new file mode 100644
index 00000000..987dbc3f
--- /dev/null
+++ b/suds/wsdl.py
@@ -0,0 +1,917 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{wsdl} module provides an objectification of the WSDL.
+The primary class is I{Definitions} as it represents the root element
+found in the document.
+"""
+
+from suds import *
+from suds.sax.element import Element
+from suds.bindings.document import Document
+from suds.bindings.rpc import RPC, Encoded
+from suds.xsd import qualify, Namespace
+from suds.xsd.schema import Schema, SchemaCollection
+from suds.xsd.query import ElementQuery
+from suds.sudsobject import Object, Facade, Metadata
+from suds.reader import DocumentReader
+
+import re
+import soaparray
+from urlparse import urljoin
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+wsdlns = (None, "http://schemas.xmlsoap.org/wsdl/")
+soapns = (None, 'http://schemas.xmlsoap.org/wsdl/soap/')
+soap12ns = (None, 'http://schemas.xmlsoap.org/wsdl/soap12/')
+
+
+class WObject(Object):
+    """
+    Base object for WSDL types.
+    @ivar root: The XML I{root} element.
+    @type root: L{Element}
+    """
+
+    def __init__(self, root):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        """
+        Object.__init__(self)
+        self.root = root
+        pmd = Metadata()
+        pmd.excludes = ['root']
+        pmd.wrappers = dict(qname=repr)
+        self.__metadata__.__print__ = pmd
+
+    def resolve(self, definitions):
+        """
+        Resolve named references to other WSDL objects.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        pass
+
+
+class NamedObject(WObject):
+    """
+    A B{named} WSDL object.
+    @ivar name: The name of the object.
+    @type name: str
+    @ivar qname: The I{qualified} name of the object.
+    @type qname: (name, I{namespace-uri}).
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        WObject.__init__(self, root)
+        self.name = root.get('name')
+        self.qname = (self.name, definitions.tns[1])
+        pmd = self.__metadata__.__print__
+        pmd.wrappers['qname'] = repr
+
+
+class Definitions(WObject):
+    """
+    I{root} container for all the WSDL objects as defined by
+    <wsdl:definitions/>
+    @ivar id: The object id.
+    @type id: str
+    @ivar options: An options dictionary.
+    @type options: L{options.Options}
+    @ivar url: The URL used to load the object.
+    @type url: str
+    @ivar tns: The target namespace for the WSDL.
+    @type tns: str
+    @ivar schema: The collective WSDL schema object.
+    @type schema: L{SchemaCollection}
+    @ivar children: The raw list of child objects.
+    @type children: [L{WObject},...]
+    @ivar imports: The list of L{Import} children.
+    @type imports: [L{Import},...]
+    @ivar messages: The dictionary of L{Message} children key'd by I{qname}
+    @type messages: [L{Message},...]
+    @ivar port_types: The dictionary of L{PortType} children key'd by I{qname}
+    @type port_types: [L{PortType},...]
+    @ivar bindings: The dictionary of L{Binding} children key'd by I{qname}
+    @type bindings: [L{Binding},...]
+    @ivar service: The service object.
+    @type service: L{Service}
+    """
+
+    Tag = 'definitions'
+
+    def __init__(self, url, options):
+        """
+        @param url: A URL to the WSDL.
+        @type url: str
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        """
+        log.debug('reading WSDL at: %s ...', url)
+        reader = DocumentReader(options)
+        d = reader.open(url)
+        root = d.root()
+        WObject.__init__(self, root)
+        self.id = objid(self)
+        self.options = options
+        self.url = url
+        self.tns = self.mktns(root)
+        self.types = []
+        self.schema = None
+        self.children = []
+        self.imports = []
+        self.messages = {}
+        self.port_types = {}
+        self.bindings = {}
+        self.services = []
+        self.add_children(self.root)
+        self.children.sort()
+        pmd = self.__metadata__.__print__
+        pmd.excludes.append('children')
+        pmd.excludes.append('wsdl')
+        pmd.wrappers['schema'] = repr
+        self.open_imports()
+        self.resolve()
+        self.build_schema()
+        self.set_wrapped()
+        for s in self.services:
+            self.add_methods(s)
+        log.debug("WSDL at '%s' loaded:\n%s", url, self)
+
+    def mktns(self, root):
+        """ Get/create the target namespace """
+        tns = root.get('targetNamespace')
+        prefix = root.findPrefix(tns)
+        if prefix is None:
+            log.debug('warning: tns (%s), not mapped to prefix', tns)
+            prefix = 'tns'
+        return (prefix, tns)
+
+    def add_children(self, root):
+        """ Add child objects using the factory """
+        for c in root.getChildren(ns=wsdlns):
+            child = Factory.create(c, self)
+            if child is None: continue
+            self.children.append(child)
+            if isinstance(child, Import):
+                self.imports.append(child)
+                continue
+            if isinstance(child, Types):
+                self.types.append(child)
+                continue
+            if isinstance(child, Message):
+                self.messages[child.qname] = child
+                continue
+            if isinstance(child, PortType):
+                self.port_types[child.qname] = child
+                continue
+            if isinstance(child, Binding):
+                self.bindings[child.qname] = child
+                continue
+            if isinstance(child, Service):
+                self.services.append(child)
+                continue
+
+    def open_imports(self):
+        """ Import the I{imported} WSDLs. """
+        for imp in self.imports:
+            imp.load(self)
+
+    def resolve(self):
+        """ Tell all children to resolve themselves """
+        for c in self.children:
+            c.resolve(self)
+
+    def build_schema(self):
+        """ Process L{Types} objects and create the schema collection """
+        container = SchemaCollection(self)
+        for t in [t for t in self.types if t.local()]:
+            for root in t.contents():
+                schema = Schema(root, self.url, self.options, container)
+                container.add(schema)
+        if not len(container):
+            root = Element.buildPath(self.root, 'types/schema')
+            schema = Schema(root, self.url, self.options, container)
+            container.add(schema)
+        self.schema = container.load(self.options)
+        for s in [t.schema() for t in self.types if t.imported()]:
+            self.schema.merge(s)
+        return self.schema
+
+    def add_methods(self, service):
+        """ Build method view for service """
+        bindings = {
+            'document/literal' : Document(self),
+            'rpc/literal' : RPC(self),
+            'rpc/encoded' : Encoded(self)
+        }
+        for p in service.ports:
+            binding = p.binding
+            ptype = p.binding.type
+            operations = p.binding.type.operations.values()
+            for name in [op.name for op in operations]:
+                m = Facade('Method')
+                m.name = name
+                m.location = p.location
+                m.binding = Facade('binding')
+                op = binding.operation(name)
+                m.soap = op.soap
+                key = '/'.join((op.soap.style, op.soap.input.body.use))
+                m.binding.input = bindings.get(key)
+                key = '/'.join((op.soap.style, op.soap.output.body.use))
+                m.binding.output = bindings.get(key)
+                op = ptype.operation(name)
+                p.methods[name] = m
+
+    def set_wrapped(self):
+        """ set (wrapped|bare) flag on messages """
+        for b in self.bindings.values():
+            for op in b.operations.values():
+                for body in (op.soap.input.body, op.soap.output.body):
+                    body.wrapped = False
+                    if not self.options.unwrap:
+                        continue
+                    if len(body.parts) != 1:
+                        continue
+                    for p in body.parts:
+                        if p.element is None:
+                            continue
+                        query = ElementQuery(p.element)
+                        pt = query.execute(self.schema)
+                        if pt is None:
+                            raise TypeNotFound(query.ref)
+                        resolved = pt.resolve()
+                        if resolved.builtin():
+                            continue
+                        body.wrapped = True
+
+    def __getstate__(self):
+        nopickle = ('options',)
+        state = self.__dict__.copy()
+        for k in nopickle:
+            if k in state:
+                del state[k]
+        return state
+
+    def __repr__(self):
+        return 'Definitions (id=%s)' % self.id
+
+
+class Import(WObject):
+    """
+    Represents the <wsdl:import/>.
+    @ivar location: The value of the I{location} attribute.
+    @type location: str
+    @ivar ns: The value of the I{namespace} attribute.
+    @type ns: str
+    @ivar imported: The imported object.
+    @type imported: L{Definitions}
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        WObject.__init__(self, root)
+        self.location = root.get('location')
+        self.ns = root.get('namespace')
+        self.imported = None
+        pmd = self.__metadata__.__print__
+        pmd.wrappers['imported'] = repr
+
+    def load(self, definitions):
+        """ Load the object by opening the URL """
+        url = self.location
+        log.debug('importing (%s)', url)
+        if '://' not in url:
+            url = urljoin(definitions.url, url)
+        options = definitions.options
+        d = Definitions(url, options)
+        if d.root.match(Definitions.Tag, wsdlns):
+            self.import_definitions(definitions, d)
+            return
+        if d.root.match(Schema.Tag, Namespace.xsdns):
+            self.import_schema(definitions, d)
+            return
+        raise Exception('document at "%s" is unknown' % url)
+
+    def import_definitions(self, definitions, d):
+        """ import/merge WSDL definitions """
+        definitions.types += d.types
+        definitions.messages.update(d.messages)
+        definitions.port_types.update(d.port_types)
+        definitions.bindings.update(d.bindings)
+        self.imported = d
+        log.debug('imported (WSDL):\n%s', d)
+
+    def import_schema(self, definitions, d):
+        """ import schema as <types/> content """
+        if not len(definitions.types):
+            root = Element('types', ns=wsdlns)
+            definitions.root.insert(root)
+            types = Types(root, definitions)
+            definitions.types.append(types)
+        else:
+            types = definitions.types[-1]
+        types.root.append(d.root)
+        log.debug('imported (XSD):\n%s', d.root)
+
+    def __gt__(self, other):
+        return False
+
+
+class Types(WObject):
+    """
+    Represents <types><schema/></types>.
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        WObject.__init__(self, root)
+        self.definitions = definitions
+
+    def contents(self):
+        return self.root.getChildren('schema', Namespace.xsdns)
+
+    def schema(self):
+        return self.definitions.schema
+
+    def local(self):
+        return ( self.definitions.schema is None )
+
+    def imported(self):
+        return ( not self.local() )
+
+    def __gt__(self, other):
+        return isinstance(other, Import)
+
+
+class Part(NamedObject):
+    """
+    Represents <message><part/></message>.
+    @ivar element: The value of the {element} attribute.
+        Stored as a I{qref} as converted by L{suds.xsd.qualify}.
+    @type element: str
+    @ivar type: The value of the {type} attribute.
+        Stored as a I{qref} as converted by L{suds.xsd.qualify}.
+    @type type: str
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        NamedObject.__init__(self, root, definitions)
+        pmd = Metadata()
+        pmd.wrappers = dict(element=repr, type=repr)
+        self.__metadata__.__print__ = pmd
+        tns = definitions.tns
+        self.element = self.__getref('element', tns)
+        self.type = self.__getref('type', tns)
+
+    def __getref(self, a, tns):
+        """ Get the qualified value of attribute named 'a'."""
+        s = self.root.get(a)
+        if s is None:
+            return s
+        else:
+            return qualify(s, self.root, tns)
+
+
+class Message(NamedObject):
+    """
+    Represents <message/>.
+    @ivar parts: A list of message parts.
+    @type parts: [I{Part},...]
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        NamedObject.__init__(self, root, definitions)
+        self.parts = []
+        for p in root.getChildren('part'):
+            part = Part(p, definitions)
+            self.parts.append(part)
+
+    def __gt__(self, other):
+        return isinstance(other, (Import, Types))
+
+
+class PortType(NamedObject):
+    """
+    Represents <portType/>.
+    @ivar operations: A list of contained operations.
+    @type operations: list
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        NamedObject.__init__(self, root, definitions)
+        self.operations = {}
+        for c in root.getChildren('operation'):
+            op = Facade('Operation')
+            op.name = c.get('name')
+            op.tns = definitions.tns
+            input = c.getChild('input')
+            if input is None:
+                op.input = None
+            else:
+                op.input = input.get('message')
+            output = c.getChild('output')
+            if output is None:
+                op.output = None
+            else:
+                op.output = output.get('message')
+            faults = []
+            for fault in c.getChildren('fault'):
+                f = Facade('Fault')
+                f.name = fault.get('name')
+                f.message = fault.get('message')
+                faults.append(f)
+            op.faults = faults
+            self.operations[op.name] = op
+
+    def resolve(self, definitions):
+        """
+        Resolve named references to other WSDL objects.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        for op in self.operations.values():
+            if op.input is None:
+                op.input = Message(Element('no-input'), definitions)
+            else:
+                qref = qualify(op.input, self.root, definitions.tns)
+                msg = definitions.messages.get(qref)
+                if msg is None:
+                    raise Exception("msg '%s', not-found" % op.input)
+                else:
+                    op.input = msg
+            if op.output is None:
+                op.output = Message(Element('no-output'), definitions)
+            else:
+                qref = qualify(op.output, self.root, definitions.tns)
+                msg = definitions.messages.get(qref)
+                if msg is None:
+                    raise Exception("msg '%s', not-found" % op.output)
+                else:
+                    op.output = msg
+            for f in op.faults:
+                qref = qualify(f.message, self.root, definitions.tns)
+                msg = definitions.messages.get(qref)
+                if msg is None:
+                    raise Exception, "msg '%s', not-found" % f.message
+                f.message = msg
+
+    def operation(self, name):
+        """
+        Shortcut used to get a contained operation by name.
+        @param name: An operation name.
+        @type name: str
+        @return: The named operation.
+        @rtype: Operation
+        @raise L{MethodNotFound}: When not found.
+        """
+        try:
+            return self.operations[name]
+        except Exception, e:
+            raise MethodNotFound(name)
+
+    def __gt__(self, other):
+        return isinstance(other, (Import, Types, Message))
+
+
+class Binding(NamedObject):
+    """
+    Represents <binding/>
+    @ivar operations: A list of contained operations.
+    @type operations: list
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        NamedObject.__init__(self, root, definitions)
+        self.operations = {}
+        self.type = root.get('type')
+        sr = self.soaproot()
+        if sr is None:
+            self.soap = None
+            log.debug('binding: "%s" not a SOAP binding', self.name)
+            return
+        soap = Facade('soap')
+        self.soap = soap
+        self.soap.style = sr.get('style', default='document')
+        self.add_operations(self.root, definitions)
+
+    def soaproot(self):
+        """ get the soap:binding """
+        for ns in (soapns, soap12ns):
+            sr = self.root.getChild('binding', ns=ns)
+            if sr is not None:
+                return sr
+        return None
+
+    def add_operations(self, root, definitions):
+        """ Add <operation/> children """
+        dsop = Element('operation', ns=soapns)
+        for c in root.getChildren('operation'):
+            op = Facade('Operation')
+            op.name = c.get('name')
+            sop = c.getChild('operation', default=dsop)
+            soap = Facade('soap')
+            soap.action = '"%s"' % sop.get('soapAction', default='')
+            soap.style = sop.get('style', default=self.soap.style)
+            soap.input = Facade('Input')
+            soap.input.body = Facade('Body')
+            soap.input.headers = []
+            soap.output = Facade('Output')
+            soap.output.body = Facade('Body')
+            soap.output.headers = []
+            op.soap = soap
+            input = c.getChild('input')
+            if input is None:
+                input = Element('input', ns=wsdlns)
+            body = input.getChild('body')
+            self.body(definitions, soap.input.body, body)
+            for header in input.getChildren('header'):
+                self.header(definitions, soap.input, header)
+            output = c.getChild('output')
+            if output is None:
+                output = Element('output', ns=wsdlns)
+            body = output.getChild('body')
+            self.body(definitions, soap.output.body, body)
+            for header in output.getChildren('header'):
+                self.header(definitions, soap.output, header)
+            faults = []
+            for fault in c.getChildren('fault'):
+                sf = fault.getChild('fault')
+                if sf is None:
+                    continue
+                fn = fault.get('name')
+                f = Facade('Fault')
+                f.name = sf.get('name', default=fn)
+                f.use = sf.get('use', default='literal')
+                faults.append(f)
+            soap.faults = faults
+            self.operations[op.name] = op
+
+    def body(self, definitions, body, root):
+        """ add the input/output body properties """
+        if root is None:
+            body.use = 'literal'
+            body.namespace = definitions.tns
+            body.parts = ()
+            return
+        parts = root.get('parts')
+        if parts is None:
+            body.parts = ()
+        else:
+            body.parts = re.split('[\s,]', parts)
+        body.use = root.get('use', default='literal')
+        ns = root.get('namespace')
+        if ns is None:
+            body.namespace = definitions.tns
+        else:
+            prefix = root.findPrefix(ns, 'b0')
+            body.namespace = (prefix, ns)
+
+    def header(self, definitions, parent, root):
+        """ add the input/output header properties """
+        if root is None:
+            return
+        header = Facade('Header')
+        parent.headers.append(header)
+        header.use = root.get('use', default='literal')
+        ns = root.get('namespace')
+        if ns is None:
+            header.namespace = definitions.tns
+        else:
+            prefix = root.findPrefix(ns, 'h0')
+            header.namespace = (prefix, ns)
+        msg = root.get('message')
+        if msg is not None:
+            header.message = msg
+        part = root.get('part')
+        if part is not None:
+            header.part = part
+
+    def resolve(self, definitions):
+        """
+        Resolve named references to other WSDL objects.  This includes
+        cross-linking information (from) the portType (to) the I{SOAP}
+        protocol information on the binding for each operation.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        self.resolveport(definitions)
+        for op in self.operations.values():
+            self.resolvesoapbody(definitions, op)
+            self.resolveheaders(definitions, op)
+            self.resolvefaults(definitions, op)
+
+    def resolveport(self, definitions):
+        """
+        Resolve port_type reference.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        ref = qualify(self.type, self.root, definitions.tns)
+        port_type = definitions.port_types.get(ref)
+        if port_type is None:
+            raise Exception("portType '%s', not-found" % self.type)
+        else:
+            self.type = port_type
+
+    def resolvesoapbody(self, definitions, op):
+        """
+        Resolve SOAP body I{message} parts by
+        cross-referencing with operation defined in port type.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        @param op: An I{operation} object.
+        @type op: I{operation}
+        """
+        ptop = self.type.operation(op.name)
+        if ptop is None:
+            raise Exception, \
+                "operation '%s' not defined in portType" % op.name
+        soap = op.soap
+        parts = soap.input.body.parts
+        if len(parts):
+            pts = []
+            for p in ptop.input.parts:
+                if p.name in parts:
+                    pts.append(p)
+            soap.input.body.parts = pts
+        else:
+            soap.input.body.parts = ptop.input.parts
+        parts = soap.output.body.parts
+        if len(parts):
+            pts = []
+            for p in ptop.output.parts:
+                if p.name in parts:
+                    pts.append(p)
+            soap.output.body.parts = pts
+        else:
+            soap.output.body.parts = ptop.output.parts
+
+    def resolveheaders(self, definitions, op):
+        """
+        Resolve SOAP header I{message} references.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        @param op: An I{operation} object.
+        @type op: I{operation}
+        """
+        soap = op.soap
+        headers = soap.input.headers + soap.output.headers
+        for header in headers:
+            mn = header.message
+            ref = qualify(mn, self.root, definitions.tns)
+            message = definitions.messages.get(ref)
+            if message is None:
+                raise Exception, "message'%s', not-found" % mn
+            pn = header.part
+            for p in message.parts:
+                if p.name == pn:
+                    header.part = p
+                    break
+            if pn == header.part:
+                raise Exception, \
+                    "message '%s' has not part named '%s'" % (ref, pn)
+
+    def resolvefaults(self, definitions, op):
+        """
+        Resolve SOAP fault I{message} references by
+        cross-referencing with operations defined in the port type.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        @param op: An I{operation} object.
+        @type op: I{operation}
+        """
+        ptop = self.type.operation(op.name)
+        if ptop is None:
+            raise Exception, \
+                "operation '%s' not defined in portType" % op.name
+        soap = op.soap
+        for fault in soap.faults:
+            for f in ptop.faults:
+                if f.name == fault.name:
+                    fault.parts = f.message.parts
+                    continue
+            if hasattr(fault, 'parts'):
+                continue
+            raise Exception, \
+                "fault '%s' not defined in portType '%s'" % (fault.name, self.type.name)
+
+    def operation(self, name):
+        """
+        Shortcut used to get a contained operation by name.
+        @param name: An operation name.
+        @type name: str
+        @return: The named operation.
+        @rtype: Operation
+        @raise L{MethodNotFound}: When not found.
+        """
+        try:
+            return self.operations[name]
+        except:
+            raise MethodNotFound(name)
+
+    def __gt__(self, other):
+        return ( not isinstance(other, Service) )
+
+
+class Port(NamedObject):
+    """
+    Represents a service port.
+    @ivar service: A service.
+    @type service: L{Service}
+    @ivar binding: A binding name.
+    @type binding: str
+    @ivar location: The service location (URL).
+    @type location: str
+    """
+
+    def __init__(self, root, definitions, service):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        @param service: A service object.
+        @type service: L{Service}
+        """
+        NamedObject.__init__(self, root, definitions)
+        self.__service = service
+        self.binding = root.get('binding')
+        address = root.getChild('address')
+        self.location = address is not None and address.get('location')
+        self.methods = {}
+
+    def method(self, name):
+        """
+        Get a method defined in this portType by name.
+        @param name: A method name.
+        @type name: str
+        @return: The requested method object.
+        @rtype: I{Method}
+        """
+        return self.methods.get(name)
+
+
+class Service(NamedObject):
+    """
+    Represents <service/>.
+    @ivar port: The contained ports.
+    @type port: [Port,..]
+    @ivar methods: The contained methods for all ports.
+    @type methods: [Method,..]
+    """
+
+    def __init__(self, root, definitions):
+        """
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        NamedObject.__init__(self, root, definitions)
+        self.ports = []
+        for p in root.getChildren('port'):
+            port = Port(p, definitions, self)
+            self.ports.append(port)
+
+    def port(self, name):
+        """
+        Locate a port by name.
+        @param name: A port name.
+        @type name: str
+        @return: The port object.
+        @rtype: L{Port}
+        """
+        for p in self.ports:
+            if p.name == name:
+                return p
+        return None
+
+    def setlocation(self, url, names=None):
+        """
+        Override the invocation location (URL) for service method.
+        @param url: A URL location.
+        @type url: A URL.
+        @param names:  A list of method names.  None=ALL
+        @type names: [str,..]
+        """
+        for p in self.ports:
+            for m in p.methods.values():
+                if names is None or m.name in names:
+                    m.location = url
+
+    def resolve(self, definitions):
+        """
+        Resolve named references to other WSDL objects.
+        Ports without SOAP bindings are discarded.
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        """
+        filtered = []
+        for p in self.ports:
+            ref = qualify(p.binding, self.root, definitions.tns)
+            binding = definitions.bindings.get(ref)
+            if binding is None:
+                raise Exception("binding '%s', not-found" % p.binding)
+            if binding.soap is None:
+                log.debug('binding "%s" - not a SOAP binding, discarded', binding.name)
+                continue
+            p.binding = binding
+            filtered.append(p)
+        self.ports = filtered
+
+    def __gt__(self, other):
+        return True
+
+
+class Factory:
+    """
+    Simple WSDL object factory.
+    @cvar tags: Dictionary of tag->constructor mappings.
+    @type tags: dict
+    """
+
+    tags =\
+    {
+        'import' : Import,
+        'types' : Types,
+        'message' : Message,
+        'portType' : PortType,
+        'binding' : Binding,
+        'service' : Service,
+    }
+
+    @classmethod
+    def create(cls, root, definitions):
+        """
+        Create an object based on the root tag name.
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param definitions: A definitions object.
+        @type definitions: L{Definitions}
+        @return: The created object.
+        @rtype: L{WObject}
+        """
+        fn = cls.tags.get(root.name)
+        if fn is not None:
+            return fn(root, definitions)
+        else:
+            return None
diff --git a/suds/wsse.py b/suds/wsse.py
new file mode 100644
index 00000000..c2f7f524
--- /dev/null
+++ b/suds/wsse.py
@@ -0,0 +1,212 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{wsse} module provides WS-Security.
+"""
+
+from logging import getLogger
+from suds import *
+from suds.sudsobject import Object
+from suds.sax.element import Element
+from suds.sax.date import DateTime, UtcTimezone
+from datetime import datetime, timedelta
+
+try:
+    from hashlib import md5
+except ImportError:
+    # Python 2.4 compatibility
+    from md5 import md5
+
+
+dsns = \
+    ('ds',
+     'http://www.w3.org/2000/09/xmldsig#')
+wssens = \
+    ('wsse',
+     'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd')
+wsuns = \
+    ('wsu',
+     'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd')
+wsencns = \
+    ('wsenc',
+     'http://www.w3.org/2001/04/xmlenc#')
+
+
+class Security(Object):
+    """
+    WS-Security object.
+    @ivar tokens: A list of security tokens
+    @type tokens: [L{Token},...]
+    @ivar signatures: A list of signatures.
+    @type signatures: TBD
+    @ivar references: A list of references.
+    @type references: TBD
+    @ivar keys: A list of encryption keys.
+    @type keys: TBD
+    """
+
+    def __init__(self):
+        """ """
+        Object.__init__(self)
+        self.mustUnderstand = True
+        self.tokens = []
+        self.signatures = []
+        self.references = []
+        self.keys = []
+
+    def xml(self):
+        """
+        Get xml representation of the object.
+        @return: The root node.
+        @rtype: L{Element}
+        """
+        root = Element('Security', ns=wssens)
+        root.set('mustUnderstand', str(self.mustUnderstand).lower())
+        for t in self.tokens:
+            root.append(t.xml())
+        return root
+
+
+class Token(Object):
+    """ I{Abstract} security token. """
+
+    @classmethod
+    def now(cls):
+        return datetime.now()
+
+    @classmethod
+    def utc(cls):
+        return datetime.utcnow().replace(tzinfo=UtcTimezone())
+
+    @classmethod
+    def sysdate(cls):
+        utc = DateTime(self.utc())
+        return str(utc)
+
+    def __init__(self):
+            Object.__init__(self)
+
+
+class UsernameToken(Token):
+    """
+    Represents a basic I{UsernameToken} WS-Secuirty token.
+    @ivar username: A username.
+    @type username: str
+    @ivar password: A password.
+    @type password: str
+    @ivar nonce: A set of bytes to prevent replay attacks.
+    @type nonce: str
+    @ivar created: The token created.
+    @type created: L{datetime}
+    """
+
+    def __init__(self, username=None, password=None):
+        """
+        @param username: A username.
+        @type username: str
+        @param password: A password.
+        @type password: str
+        """
+        Token.__init__(self)
+        self.username = username
+        self.password = password
+        self.nonce = None
+        self.created = None
+
+    def setnonce(self, text=None):
+        """
+        Set I{nonce} which is an arbitrary set of bytes to prevent replay
+        attacks.
+        @param text: The nonce text value.
+            Generated when I{None}.
+        @type text: str
+        """
+        if text is None:
+            s = []
+            s.append(self.username)
+            s.append(self.password)
+            s.append(Token.sysdate())
+            m = md5()
+            m.update(':'.join(s))
+            self.nonce = m.hexdigest()
+        else:
+            self.nonce = text
+
+    def setcreated(self, dt=None):
+        """
+        Set I{created}.
+        @param dt: The created date & time.
+            Set as datetime.utc() when I{None}.
+        @type dt: L{datetime}
+        """
+        if dt is None:
+            self.created = Token.utc()
+        else:
+            self.created = dt
+
+
+    def xml(self):
+        """
+        Get xml representation of the object.
+        @return: The root node.
+        @rtype: L{Element}
+        """
+        root = Element('UsernameToken', ns=wssens)
+        u = Element('Username', ns=wssens)
+        u.setText(self.username)
+        root.append(u)
+        p = Element('Password', ns=wssens)
+        p.setText(self.password)
+        root.append(p)
+        if self.nonce is not None:
+            n = Element('Nonce', ns=wssens)
+            n.setText(self.nonce)
+            root.append(n)
+        if self.created is not None:
+            n = Element('Created', ns=wsuns)
+            n.setText(str(DateTime(self.created)))
+            root.append(n)
+        return root
+
+
+class Timestamp(Token):
+    """
+    Represents the I{Timestamp} WS-Secuirty token.
+    @ivar created: The token created.
+    @type created: L{datetime}
+    @ivar expires: The token expires.
+    @type expires: L{datetime}
+    """
+
+    def __init__(self, validity=90):
+        """
+        @param validity: The time in seconds.
+        @type validity: int
+        """
+        Token.__init__(self)
+        self.created = Token.utc()
+        self.expires = self.created + timedelta(seconds=validity)
+
+    def xml(self):
+        root = Element("Timestamp", ns=wsuns)
+        created = Element('Created', ns=wsuns)
+        created.setText(str(DateTime(self.created)))
+        expires = Element('Expires', ns=wsuns)
+        expires.setText(str(DateTime(self.expires)))
+        root.append(created)
+        root.append(expires)
+        return root
diff --git a/suds/xsd/__init__.py b/suds/xsd/__init__.py
new file mode 100644
index 00000000..c5d80154
--- /dev/null
+++ b/suds/xsd/__init__.py
@@ -0,0 +1,75 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+
+from suds import *
+from suds.sax import Namespace, splitPrefix
+
+
+def qualify(ref, resolvers, defns=Namespace.default):
+    """
+    Get a reference that is I{qualified} by namespace.
+    @param ref: A referenced schema type name.
+    @type ref: str
+    @param resolvers: A list of objects to be used to resolve types.
+    @type resolvers: [L{sax.element.Element},]
+    @param defns: An optional target namespace used to qualify references
+        when no prefix is specified.
+    @type defns: A default namespace I{tuple: (prefix,uri)} used when ref not prefixed.
+    @return: A qualified reference.
+    @rtype: (name, namespace-uri)
+    """
+    ns = None
+    p, n = splitPrefix(ref)
+    if p is not None:
+        if not isinstance(resolvers, (list, tuple)):
+            resolvers = (resolvers,)
+        for r in resolvers:
+            resolved = r.resolvePrefix(p)
+            if resolved[1] is not None:
+                ns = resolved
+                break
+        if ns is None:
+            raise Exception('prefix (%s) not resolved' % p)
+    else:
+        ns = defns
+    return (n, ns[1])
+
+def isqref(object):
+    """
+    Get whether the object is a I{qualified reference}.
+    @param object: An object to be tested.
+    @type object: I{any}
+    @rtype: boolean
+    @see: L{qualify}
+    """
+    return (\
+        isinstance(object, tuple) and \
+        len(object) == 2 and \
+        isinstance(object[0], basestring) and \
+        isinstance(object[1], basestring))
+
+
+class Filter:
+    def __init__(self, inclusive=False, *items):
+        self.inclusive = inclusive
+        self.items = items
+    def __contains__(self, x):
+        if self.inclusive:
+            result = ( x in self.items )
+        else:
+            result = ( x not in self.items )
+        return result
diff --git a/suds/xsd/deplist.py b/suds/xsd/deplist.py
new file mode 100644
index 00000000..b813f80f
--- /dev/null
+++ b/suds/xsd/deplist.py
@@ -0,0 +1,140 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{depsolve} module defines a class for performing dependency solving.
+"""
+
+from suds import *
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class DepList:
+    """
+    Dependency solving list.
+    Items are tuples: (object, (deps,))
+    @ivar raw: The raw (unsorted) items.
+    @type raw: list
+    @ivar index: The index of (unsorted) items.
+    @type index: list
+    @ivar stack: The sorting stack.
+    @type stack: list
+    @ivar pushed: The I{pushed} set tracks items that have been
+        processed.
+    @type pushed: set
+    @ivar sorted: The sorted list of items.
+    @type sorted: list
+    """
+
+    def __init__(self):
+        """ """
+        self.unsorted = []
+        self.index = {}
+        self.stack = []
+        self.pushed = set()
+        self.sorted = None
+
+    def add(self, *items):
+        """
+        Add items to be sorted.
+        @param items: One or more items to be added.
+        @type items: I{item}
+        @return: self
+        @rtype: L{DepList}
+        """
+        for item in items:
+            self.unsorted.append(item)
+            key = item[0]
+            self.index[key] = item
+        return self
+
+    def sort(self):
+        """
+        Sort the list based on dependencies.
+        @return: The sorted items.
+        @rtype: list
+        """
+        self.sorted = list()
+        self.pushed = set()
+        for item in self.unsorted:
+            popped = []
+            self.push(item)
+            while len(self.stack):
+                try:
+                    top = self.top()
+                    ref = top[1].next()
+                    refd = self.index.get(ref)
+                    if refd is None:
+                        log.debug('"%s" not found, skipped', Repr(ref))
+                        continue
+                    self.push(refd)
+                except StopIteration:
+                    popped.append(self.pop())
+                    continue
+            for p in popped:
+                self.sorted.append(p)
+        self.unsorted = self.sorted
+        return self.sorted
+
+    def top(self):
+        """
+        Get the item at the top of the stack.
+        @return: The top item.
+        @rtype: (item, iter)
+        """
+        return self.stack[-1]
+
+    def push(self, item):
+        """
+        Push and item onto the sorting stack.
+        @param item: An item to push.
+        @type item: I{item}
+        @return: The number of items pushed.
+        @rtype: int
+        """
+        if item in self.pushed:
+            return
+        frame = (item, iter(item[1]))
+        self.stack.append(frame)
+        self.pushed.add(item)
+
+    def pop(self):
+        """
+        Pop the top item off the stack and append
+        it to the sorted list.
+        @return: The popped item.
+        @rtype: I{item}
+        """
+        try:
+            frame = self.stack.pop()
+            return frame[0]
+        except:
+            pass
+
+
+if __name__ == '__main__':
+    a = ('a', ('x',))
+    b = ('b', ('a',))
+    c = ('c', ('a','b'))
+    d = ('d', ('c',))
+    e = ('e', ('d','a'))
+    f = ('f', ('e','c','d','a'))
+    x = ('x', ())
+    L = DepList()
+    L.add(c, e, d, b, f, a, x)
+    print [x[0] for x in L.sort()]
diff --git a/suds/xsd/doctor.py b/suds/xsd/doctor.py
new file mode 100644
index 00000000..5a52e76c
--- /dev/null
+++ b/suds/xsd/doctor.py
@@ -0,0 +1,223 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{doctor} module provides classes for fixing broken (sick)
+schema(s).
+"""
+
+from suds.sax import Namespace
+from suds.sax.element import Element
+from suds.plugin import DocumentPlugin, DocumentContext
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Doctor:
+    """
+    Schema Doctor.
+    """
+    def examine(self, root):
+        """
+        Examine and repair the schema (if necessary).
+        @param root: A schema root element.
+        @type root: L{Element}
+        """
+        pass
+
+
+class Practice(Doctor):
+    """
+    A collection of doctors.
+    @ivar doctors: A list of doctors.
+    @type doctors: list
+    """
+
+    def __init__(self):
+        self.doctors = []
+
+    def add(self, doctor):
+        """
+        Add a doctor to the practice
+        @param doctor: A doctor to add.
+        @type doctor: L{Doctor}
+        """
+        self.doctors.append(doctor)
+
+    def examine(self, root):
+        for d in self.doctors:
+            d.examine(root)
+        return root
+
+
+class TnsFilter:
+    """
+    Target Namespace filter.
+    @ivar tns: A list of target namespaces.
+    @type tns: [str,...]
+    """
+
+    def __init__(self, *tns):
+        """
+        @param tns: A list of target namespaces.
+        @type tns: [str,...]
+        """
+        self.tns = []
+        self.add(*tns)
+
+    def add(self, *tns):
+        """
+        Add I{targetNamespaces} to be added.
+        @param tns: A list of target namespaces.
+        @type tns: [str,...]
+        """
+        self.tns += tns
+
+    def match(self, root, ns):
+        """
+        Match by I{targetNamespace} excluding those that
+        are equal to the specified namespace to prevent
+        adding an import to itself.
+        @param root: A schema root.
+        @type root: L{Element}
+        """
+        tns = root.get('targetNamespace')
+        if len(self.tns):
+            matched = ( tns in self.tns )
+        else:
+            matched = 1
+        itself = ( ns == tns )
+        return ( matched and not itself )
+
+
+class Import:
+    """
+    An <xs:import/> to be applied.
+    @cvar xsdns: The XSD namespace.
+    @type xsdns: (p,u)
+    @ivar ns: An import namespace.
+    @type ns: str
+    @ivar location: An optional I{schemaLocation}.
+    @type location: str
+    @ivar filter: A filter used to restrict application to
+        a particular schema.
+    @type filter: L{TnsFilter}
+    """
+
+    xsdns = Namespace.xsdns
+
+    def __init__(self, ns, location=None):
+        """
+        @param ns: An import namespace.
+        @type ns: str
+        @param location: An optional I{schemaLocation}.
+        @type location: str
+        """
+        self.ns = ns
+        self.location = location
+        self.filter = TnsFilter()
+
+    def setfilter(self, filter):
+        """
+        Set the filter.
+        @param filter: A filter to set.
+        @type filter: L{TnsFilter}
+        """
+        self.filter = filter
+
+    def apply(self, root):
+        """
+        Apply the import (rule) to the specified schema.
+        If the schema does not already contain an import for the
+        I{namespace} specified here, it is added.
+        @param root: A schema root.
+        @type root: L{Element}
+        """
+        if not self.filter.match(root, self.ns):
+            return
+        if self.exists(root):
+            return
+        node = Element('import', ns=self.xsdns)
+        node.set('namespace', self.ns)
+        if self.location is not None:
+            node.set('schemaLocation', self.location)
+        log.debug('inserting: %s', node)
+        root.insert(node)
+
+    def add(self, root):
+        """
+        Add an <xs:import/> to the specified schema root.
+        @param root: A schema root.
+        @type root: L{Element}
+        """
+        node = Element('import', ns=self.xsdns)
+        node.set('namespace', self.ns)
+        if self.location is not None:
+            node.set('schemaLocation', self.location)
+        log.debug('%s inserted', node)
+        root.insert(node)
+
+    def exists(self, root):
+        """
+        Check to see if the <xs:import/> already exists
+        in the specified schema root by matching I{namespace}.
+        @param root: A schema root.
+        @type root: L{Element}
+        """
+        for node in root.children:
+            if node.name != 'import':
+                continue
+            ns = node.get('namespace')
+            if self.ns == ns:
+                return 1
+        return 0
+
+
+class ImportDoctor(Doctor, DocumentPlugin):
+    """
+    Doctor used to fix missing imports.
+    @ivar imports: A list of imports to apply.
+    @type imports: [L{Import},...]
+    """
+
+    def __init__(self, *imports):
+        self.imports = []
+        self.add(*imports)
+
+    def add(self, *imports):
+        """
+        Add a namespace to be checked.
+        @param imports: A list of L{Import} objects.
+        @type imports: [L{Import},..]
+        """
+        self.imports += imports
+
+    def examine(self, node):
+        for imp in self.imports:
+            imp.apply(node)
+
+    def parsed(self, context):
+        node = context.document
+        # xsd root
+        if node.name == 'schema' and Namespace.xsd(node.namespace()):
+            self.examine(node)
+            return
+        # look deeper
+        context = DocumentContext()
+        for child in node:
+            context.document = child
+            self.parsed(context)
diff --git a/suds/xsd/query.py b/suds/xsd/query.py
new file mode 100644
index 00000000..8f2266b1
--- /dev/null
+++ b/suds/xsd/query.py
@@ -0,0 +1,208 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{query} module defines a class for performing schema queries.
+"""
+
+from suds import *
+from suds.sudsobject import *
+from suds.xsd import qualify, isqref
+from suds.xsd.sxbuiltin import Factory
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class Query(Object):
+    """
+    Schema query base class.
+
+    """
+    def __init__(self, ref=None):
+        """
+        @param ref: The schema reference being queried.
+        @type ref: qref
+        """
+        Object.__init__(self)
+        self.id = objid(self)
+        self.ref = ref
+        self.history = []
+        self.resolved = False
+        if not isqref(self.ref):
+            raise Exception('%s, must be qref' % tostr(self.ref))
+
+    def execute(self, schema):
+        """
+        Execute this query using the specified schema.
+        @param schema: The schema associated with the query. The schema is used
+            by the query to search for items.
+        @type schema: L{schema.Schema}
+        @return: The item matching the search criteria.
+        @rtype: L{sxbase.SchemaObject}
+        """
+        raise Exception, 'not-implemented by subclass'
+
+    def filter(self, result):
+        """
+        Filter the specified result based on query criteria.
+        @param result: A potential result.
+        @type result: L{sxbase.SchemaObject}
+        @return: True if result should be excluded.
+        @rtype: boolean
+        """
+        if result is None:
+            return True
+        reject = ( result in self.history )
+        if reject:
+            log.debug('result %s, rejected by\n%s', Repr(result), self)
+        return reject
+
+    def result(self, result):
+        """
+        Query result post processing.
+        @param result: A query result.
+        @type result: L{sxbase.SchemaObject}
+        """
+        if result is None:
+            log.debug('%s, not-found', self.ref)
+            return
+        if self.resolved:
+            result = result.resolve()
+        log.debug('%s, found as: %s', self.ref, Repr(result))
+        self.history.append(result)
+        return result
+
+
+class BlindQuery(Query):
+    """
+    Schema query class that I{blindly} searches for a reference in the
+    specified schema. It may be used to find Elements and Types but will match
+    on an Element first. This query will also find builtins.
+
+    """
+    def execute(self, schema):
+        if schema.builtin(self.ref):
+            name = self.ref[0]
+            b = Factory.create(schema, name)
+            log.debug('%s, found builtin (%s)', self.id, name)
+            return b
+        result = None
+        for d in (schema.elements, schema.types):
+            result = d.get(self.ref)
+            if self.filter(result):
+                result = None
+            else:
+                break
+        if result is None:
+            eq = ElementQuery(self.ref)
+            eq.history = self.history
+            result = eq.execute(schema)
+        return self.result(result)
+
+
+class TypeQuery(Query):
+    """
+    Schema query class that searches for Type references in the specified
+    schema. Matches on root types only.
+
+    """
+    def execute(self, schema):
+        if schema.builtin(self.ref):
+            name = self.ref[0]
+            b = Factory.create(schema, name)
+            log.debug('%s, found builtin (%s)', self.id, name)
+            return b
+        result = schema.types.get(self.ref)
+        if self.filter(result):
+            result = None
+        return self.result(result)
+
+
+class GroupQuery(Query):
+    """
+    Schema query class that searches for Group references in the specified
+    schema.
+
+    """
+    def execute(self, schema):
+        result = schema.groups.get(self.ref)
+        if self.filter(result):
+            result = None
+        return self.result(result)
+
+
+class AttrQuery(Query):
+    """
+    Schema query class that searches for Attribute references in the specified
+    schema. Matches on root Attribute by qname first, then searches deeper into
+    the document.
+
+    """
+    def execute(self, schema):
+        result = schema.attributes.get(self.ref)
+        if self.filter(result):
+            result = self.__deepsearch(schema)
+        return self.result(result)
+
+    def __deepsearch(self, schema):
+        from suds.xsd.sxbasic import Attribute
+        result = None
+        for e in schema.all:
+            result = e.find(self.ref, (Attribute,))
+            if self.filter(result):
+                result = None
+            else:
+                break
+        return result
+
+
+class AttrGroupQuery(Query):
+    """
+    Schema query class that searches for attributeGroup references in the
+    specified schema.
+
+    """
+    def execute(self, schema):
+        result = schema.agrps.get(self.ref)
+        if self.filter(result):
+            result = None
+        return self.result(result)
+
+
+class ElementQuery(Query):
+    """
+    Schema query class that searches for Element references in the specified
+    schema. Matches on root Elements by qname first, then searches deeper into
+    the document.
+
+    """
+    def execute(self, schema):
+        result = schema.elements.get(self.ref)
+        if self.filter(result):
+            result = self.__deepsearch(schema)
+        return self.result(result)
+
+    def __deepsearch(self, schema):
+        from suds.xsd.sxbasic import Element
+        result = None
+        for e in schema.all:
+            result = e.find(self.ref, (Element,))
+            if self.filter(result):
+                result = None
+            else:
+                break
+        return result
diff --git a/suds/xsd/schema.py b/suds/xsd/schema.py
new file mode 100644
index 00000000..00e217ff
--- /dev/null
+++ b/suds/xsd/schema.py
@@ -0,0 +1,411 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{schema} module provides an intelligent representation of
+an XSD schema.  The I{raw} model is the XML tree and the I{model}
+is the denormalized, objectified and intelligent view of the schema.
+Most of the I{value-add} provided by the model is centered around
+transparent referenced type resolution and targeted denormalization.
+"""
+
+
+from suds import *
+from suds.xsd import *
+from suds.xsd.sxbuiltin import *
+from suds.xsd.sxbasic import Factory as BasicFactory
+from suds.xsd.sxbuiltin import Factory as BuiltinFactory
+from suds.xsd.sxbase import SchemaObject
+from suds.xsd.deplist import DepList
+from suds.sax.element import Element
+from suds.sax import splitPrefix, Namespace
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class SchemaCollection(UnicodeMixin):
+    """
+    A collection of schema objects.  This class is needed because WSDLs
+    may contain more then one <schema/> node.
+    @ivar wsdl: A wsdl object.
+    @type wsdl: L{suds.wsdl.Definitions}
+    @ivar children: A list contained schemas.
+    @type children: [L{Schema},...]
+    @ivar namespaces: A dictionary of contained schemas by namespace.
+    @type namespaces: {str:L{Schema}}
+    """
+
+    def __init__(self, wsdl):
+        """
+        @param wsdl: A wsdl object.
+        @type wsdl: L{suds.wsdl.Definitions}
+        """
+        self.wsdl = wsdl
+        self.children = []
+        self.namespaces = {}
+
+    def add(self, schema):
+        """
+        Add a schema node to the collection.  Schema(s) within the same target
+        namespace are consolidated.
+        @param schema: A schema object.
+        @type schema: (L{Schema})
+        """
+        key = schema.tns[1]
+        existing = self.namespaces.get(key)
+        if existing is None:
+            self.children.append(schema)
+            self.namespaces[key] = schema
+        else:
+            existing.root.children += schema.root.children
+            existing.root.nsprefixes.update(schema.root.nsprefixes)
+
+    def load(self, options):
+        """
+        Load the schema objects for the root nodes.
+            - de-references schemas
+            - merge schemas
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        @return: The merged schema.
+        @rtype: L{Schema}
+        """
+        if options.autoblend:
+            self.autoblend()
+        for child in self.children:
+            child.build()
+        for child in self.children:
+            child.open_imports(options)
+        for child in self.children:
+            child.dereference()
+        log.debug('loaded:\n%s', self)
+        merged = self.merge()
+        log.debug('MERGED:\n%s', merged)
+        return merged
+
+    def autoblend(self):
+        """
+        Ensure that all schemas within the collection
+        import each other which has a blending effect.
+        @return: self
+        @rtype: L{SchemaCollection}
+        """
+        namespaces = self.namespaces.keys()
+        for s in self.children:
+            for ns in namespaces:
+                tns = s.root.get('targetNamespace')
+                if tns == ns:
+                    continue
+                for imp in s.root.getChildren('import'):
+                    if imp.get('namespace') == ns:
+                        continue
+                imp = Element('import', ns=Namespace.xsdns)
+                imp.set('namespace', ns)
+                s.root.append(imp)
+        return self
+
+    def locate(self, ns):
+        """
+        Find a schema by namespace.  Only the URI portion of
+        the namespace is compared to each schema's I{targetNamespace}
+        @param ns: A namespace.
+        @type ns: (prefix,URI)
+        @return: The schema matching the namespace, else None.
+        @rtype: L{Schema}
+        """
+        return self.namespaces.get(ns[1])
+
+    def merge(self):
+        """
+        Merge the contained schemas into one.
+        @return: The merged schema.
+        @rtype: L{Schema}
+        """
+        if len(self):
+            schema = self.children[0]
+            for s in self.children[1:]:
+                schema.merge(s)
+            return schema
+        else:
+            return None
+
+    def __len__(self):
+        return len(self.children)
+
+    def __unicode__(self):
+        result = ['\nschema collection']
+        for s in self.children:
+            result.append(s.str(1))
+        return '\n'.join(result)
+
+
+class Schema(UnicodeMixin):
+    """
+    The schema is an objectification of a <schema/> (xsd) definition.
+    It provides inspection, lookup and type resolution.
+    @ivar root: The root node.
+    @type root: L{sax.element.Element}
+    @ivar baseurl: The I{base} URL for this schema.
+    @type baseurl: str
+    @ivar container: A schema collection containing this schema.
+    @type container: L{SchemaCollection}
+    @ivar children: A list of direct top level children.
+    @type children: [L{SchemaObject},...]
+    @ivar all: A list of all (includes imported) top level children.
+    @type all: [L{SchemaObject},...]
+    @ivar types: A schema types cache.
+    @type types: {name:L{SchemaObject}}
+    @ivar imports: A list of import objects.
+    @type imports: [L{SchemaObject},...]
+    @ivar elements: A list of <element/> objects.
+    @type elements: [L{SchemaObject},...]
+    @ivar attributes: A list of <attribute/> objects.
+    @type attributes: [L{SchemaObject},...]
+    @ivar groups: A list of group objects.
+    @type groups: [L{SchemaObject},...]
+    @ivar agrps: A list of attribute group objects.
+    @type agrps: [L{SchemaObject},...]
+    @ivar form_qualified: The flag indicating:
+        (@elementFormDefault).
+    @type form_qualified: bool
+    """
+
+    Tag = 'schema'
+
+    def __init__(self, root, baseurl, options, container=None):
+        """
+        @param root: The xml root.
+        @type root: L{sax.element.Element}
+        @param baseurl: The base url used for importing.
+        @type baseurl: basestring
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        @param container: An optional container.
+        @type container: L{SchemaCollection}
+        """
+        self.root = root
+        self.id = objid(self)
+        self.tns = self.mktns()
+        self.baseurl = baseurl
+        self.container = container
+        self.children = []
+        self.all = []
+        self.types = {}
+        self.imports = []
+        self.elements = {}
+        self.attributes = {}
+        self.groups = {}
+        self.agrps = {}
+        if options.doctor is not None:
+            options.doctor.examine(root)
+        form = self.root.get('elementFormDefault')
+        if form is None:
+            self.form_qualified = False
+        else:
+            self.form_qualified = ( form == 'qualified' )
+        if container is None:
+            self.build()
+            self.open_imports(options)
+            log.debug('built:\n%s', self)
+            self.dereference()
+            log.debug('dereferenced:\n%s', self)
+
+    def mktns(self):
+        """
+        Make the schema's target namespace.
+        @return: The namespace representation of the schema's
+            targetNamespace value.
+        @rtype: (prefix, uri)
+        """
+        tns = [None, self.root.get('targetNamespace')]
+        if tns[1] is not None:
+            tns[0] = self.root.findPrefix(tns[1])
+        return tuple(tns)
+
+    def build(self):
+        """
+        Build the schema (object graph) using the root node
+        using the factory.
+            - Build the graph.
+            - Collate the children.
+        """
+        self.children = BasicFactory.build(self.root, self)
+        collated = BasicFactory.collate(self.children)
+        self.children = collated[0]
+        self.attributes = collated[2]
+        self.imports = collated[1]
+        self.elements = collated[3]
+        self.types = collated[4]
+        self.groups = collated[5]
+        self.agrps = collated[6]
+
+    def merge(self, schema):
+        """
+        Merge the contents from the schema.  Only objects not already contained
+        in this schema's collections are merged.  This is to provide for bidirectional
+        import which produce cyclic includes.
+        @returns: self
+        @rtype: L{Schema}
+        """
+        for item in schema.attributes.items():
+            if item[0] in self.attributes:
+                continue
+            self.all.append(item[1])
+            self.attributes[item[0]] = item[1]
+        for item in schema.elements.items():
+            if item[0] in self.elements:
+                continue
+            self.all.append(item[1])
+            self.elements[item[0]] = item[1]
+        for item in schema.types.items():
+            if item[0] in self.types:
+                continue
+            self.all.append(item[1])
+            self.types[item[0]] = item[1]
+        for item in schema.groups.items():
+            if item[0] in self.groups:
+                continue
+            self.all.append(item[1])
+            self.groups[item[0]] = item[1]
+        for item in schema.agrps.items():
+            if item[0] in self.agrps:
+                continue
+            self.all.append(item[1])
+            self.agrps[item[0]] = item[1]
+        schema.merged = True
+        return self
+
+    def open_imports(self, options):
+        """
+        Instruct all contained L{sxbasic.Import} children to import
+        the schema's which they reference.  The contents of the
+        imported schema are I{merged} in.
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        """
+        for imp in self.imports:
+            imported = imp.open(options)
+            if imported is None:
+                continue
+            imported.open_imports(options)
+            log.debug('imported:\n%s', imported)
+            self.merge(imported)
+
+    def dereference(self):
+        """
+        Instruct all children to perform dereferencing.
+        """
+        all = []
+        indexes = {}
+        for child in self.children:
+            child.content(all)
+        deplist = DepList()
+        for x in all:
+            x.qualify()
+            midx, deps = x.dependencies()
+            item = (x, tuple(deps))
+            deplist.add(item)
+            indexes[x] = midx
+        for x, deps in deplist.sort():
+            midx = indexes.get(x)
+            if midx is None: continue
+            d = deps[midx]
+            log.debug('(%s) merging %s <== %s', self.tns[1], Repr(x), Repr(d))
+            x.merge(d)
+
+    def locate(self, ns):
+        """
+        Find a schema by namespace.  Only the URI portion of
+        the namespace is compared to each schema's I{targetNamespace}.
+        The request is passed to the container.
+        @param ns: A namespace.
+        @type ns: (prefix,URI)
+        @return: The schema matching the namespace, else None.
+        @rtype: L{Schema}
+        """
+        if self.container is not None:
+            return self.container.locate(ns)
+        else:
+            return None
+
+    def custom(self, ref, context=None):
+        """
+        Get whether the specified reference is B{not} an (xs) builtin.
+        @param ref: A str or qref.
+        @type ref: (str|qref)
+        @return: True if B{not} a builtin, else False.
+        @rtype: bool
+        """
+        if ref is None:
+            return True
+        else:
+            return ( not self.builtin(ref, context) )
+
+    def builtin(self, ref, context=None):
+        """
+        Get whether the specified reference is an (xs) builtin.
+        @param ref: A str or qref.
+        @type ref: (str|qref)
+        @return: True if builtin, else False.
+        @rtype: bool
+        """
+        w3 = 'http://www.w3.org'
+        try:
+            if isqref(ref):
+                ns = ref[1]
+                return ( ref[0] in Factory.tags and ns.startswith(w3) )
+            if context is None:
+                context = self.root
+            prefix = splitPrefix(ref)[0]
+            prefixes = context.findPrefixes(w3, 'startswith')
+            return ( prefix in prefixes and ref[0] in Factory.tags )
+        except:
+            return False
+
+    def instance(self, root, baseurl, options):
+        """
+        Create and return an new schema object using the
+        specified I{root} and I{url}.
+        @param root: A schema root node.
+        @type root: L{sax.element.Element}
+        @param baseurl: A base URL.
+        @type baseurl: str
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        @return: The newly created schema object.
+        @rtype: L{Schema}
+        @note: This is only used by Import children.
+        """
+        return Schema(root, baseurl, options)
+
+    def str(self, indent=0):
+        tab = '%*s'%(indent*3, '')
+        result = []
+        result.append('%s%s' % (tab, self.id))
+        result.append('%s(raw)' % tab)
+        result.append(self.root.str(indent+1))
+        result.append('%s(model)' % tab)
+        for c in self.children:
+            result.append(c.str(indent+1))
+        result.append('')
+        return '\n'.join(result)
+
+    def __repr__(self):
+        return '<%s tns="%s"/>' % (self.id, self.tns[1])
+
+    def __unicode__(self):
+        return self.str()
diff --git a/suds/xsd/sxbase.py b/suds/xsd/sxbase.py
new file mode 100644
index 00000000..1ceb8236
--- /dev/null
+++ b/suds/xsd/sxbase.py
@@ -0,0 +1,661 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{sxbase} module provides I{base} classes representing schema objects.
+"""
+
+from suds import *
+from suds.xsd import *
+from suds.sax.element import Element
+from suds.sax import Namespace
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class SchemaObject(UnicodeMixin):
+    """
+    A schema object is an extension to object with schema awareness.
+    @ivar root: The XML root element.
+    @type root: L{Element}
+    @ivar schema: The schema containing this object.
+    @type schema: L{schema.Schema}
+    @ivar form_qualified: A flag that indicates that @elementFormDefault
+        has a value of I{qualified}.
+    @type form_qualified: boolean
+    @ivar nillable: A flag that indicates that @nillable
+        has a value of I{true}.
+    @type nillable: boolean
+    @ivar default: The default value.
+    @type default: object
+    @ivar rawchildren: A list raw of all children.
+    @type rawchildren: [L{SchemaObject},...]
+    """
+
+    @classmethod
+    def prepend(cls, d, s, filter=Filter()):
+        """
+        Prepend schema objects from B{s}ource list to
+        the B{d}estination list while applying the filter.
+        @param d: The destination list.
+        @type d: list
+        @param s: The source list.
+        @type s: list
+        @param filter: A filter that allows items to be prepended.
+        @type filter: L{Filter}
+        """
+        i = 0
+        for x in s:
+            if x in filter:
+                d.insert(i, x)
+                i += 1
+
+    @classmethod
+    def append(cls, d, s, filter=Filter()):
+        """
+        Append schema objects from B{s}ource list to
+        the B{d}estination list while applying the filter.
+        @param d: The destination list.
+        @type d: list
+        @param s: The source list.
+        @type s: list
+        @param filter: A filter that allows items to be appended.
+        @type filter: L{Filter}
+        """
+        for item in s:
+            if item in filter:
+                d.append(item)
+
+    def __init__(self, schema, root):
+        """
+        @param schema: The containing schema.
+        @type schema: L{schema.Schema}
+        @param root: The XML root node.
+        @type root: L{Element}
+        """
+        self.schema = schema
+        self.root = root
+        self.id = objid(self)
+        self.name = root.get('name')
+        self.qname = (self.name, schema.tns[1])
+        self.min = root.get('minOccurs')
+        self.max = root.get('maxOccurs')
+        self.type = root.get('type')
+        self.ref = root.get('ref')
+        self.form_qualified = schema.form_qualified
+        self.nillable = False
+        self.default = root.get('default')
+        self.rawchildren = []
+
+    def attributes(self, filter=Filter()):
+        """
+        Get only the attribute content.
+        @param filter: A filter to constrain the result.
+        @type filter: L{Filter}
+        @return: A list of tuples (attr, ancestry)
+        @rtype: [(L{SchemaObject}, [L{SchemaObject},..]),..]
+        """
+        result = []
+        for child, ancestry in self:
+            if child.isattr() and child in filter:
+                result.append((child, ancestry))
+        return result
+
+    def children(self, filter=Filter()):
+        """
+        Get only the I{direct} or non-attribute content.
+        @param filter: A filter to constrain the result.
+        @type filter: L{Filter}
+        @return: A list tuples: (child, ancestry)
+        @rtype: [(L{SchemaObject}, [L{SchemaObject},..]),..]
+        """
+        result = []
+        for child, ancestry in self:
+            if not child.isattr() and child in filter:
+                result.append((child, ancestry))
+        return result
+
+    def get_attribute(self, name):
+        """
+        Get (find) an attribute by name.
+        @param name: A attribute name.
+        @type name: str
+        @return: A tuple: the requested (attribute, ancestry).
+        @rtype: (L{SchemaObject}, [L{SchemaObject},..])
+        """
+        for child, ancestry in self.attributes():
+            if child.name == name:
+                return child, ancestry
+        return None, []
+
+    def get_child(self, name):
+        """
+        Get (find) a I{non-attribute} child by name.
+        @param name: A child name.
+        @type name: str
+        @return: A tuple: the requested (child, ancestry).
+        @rtype: (L{SchemaObject}, [L{SchemaObject},..])
+        """
+        for child, ancestry in self.children():
+            if child.any() or child.name == name:
+                return child, ancestry
+        return None, []
+
+    def namespace(self, prefix=None):
+        """
+        Get this property's namespace.
+        @param prefix: The default prefix.
+        @type prefix: str
+        @return: The schema's target namespace
+        @rtype: (I{prefix},I{URI})
+        """
+        ns = self.schema.tns
+        if ns[0] is None:
+            ns = (prefix, ns[1])
+        return ns
+
+    def default_namespace(self):
+        return self.root.defaultNamespace()
+
+    def multi_occurrence(self):
+        """
+        Get whether the node has multiple occurrences, i.e. is a I{collection}.
+        @return: True if it has, False if it has 1 occurrence at most.
+        @rtype: boolean
+        """
+        max = self.max
+        if max is None:
+            return False
+        if max.isdigit():
+            return int(max) > 1
+        return max == 'unbounded'
+
+    def optional(self):
+        """
+        Get whether this type is optional.
+        @return: True if optional, else False
+        @rtype: boolean
+        """
+        return self.min == '0'
+
+    def required(self):
+        """
+        Get whether this type is required.
+        @return: True if required, else False
+        @rtype: boolean
+        """
+        return not self.optional()
+
+    def resolve(self, nobuiltin=False):
+        """
+        Resolve the node's type reference and return the referenced type node.
+
+        Only schema objects that actually support 'having a type' custom
+        implement this interface while others simply resolve as themselves.
+        @param nobuiltin: Flag indicating whether resolving to an external XSD
+            builtin type should not be allowed.
+        @return: The resolved (true) type.
+        @rtype: L{SchemaObject}
+        """
+        return self
+
+    def sequence(self):
+        """
+        Get whether this is a <xs:sequence/>.
+        @return: True if <xs:sequence/>, else False
+        @rtype: boolean
+        """
+        return False
+
+    def xslist(self):
+        """
+        Get whether this is a <xs:list/>.
+        @return: True if any, else False
+        @rtype: boolean
+        """
+        return False
+
+    def all(self):
+        """
+        Get whether this is an <xs:all/>.
+        @return: True if any, else False
+        @rtype: boolean
+        """
+        return False
+
+    def choice(self):
+        """
+        Get whether this is a <xs:choice/>.
+        @return: True if any, else False
+        @rtype: boolean
+        """
+        return False
+
+    def any(self):
+        """
+        Get whether this is an <xs:any/>.
+        @return: True if any, else False
+        @rtype: boolean
+        """
+        return False
+
+    def builtin(self):
+        """
+        Get whether this is a schema-instance (xs) type.
+        @return: True if any, else False
+        @rtype: boolean
+        """
+        return False
+
+    def enum(self):
+        """
+        Get whether this is a simple-type containing an enumeration.
+        @return: True if any, else False
+        @rtype: boolean
+        """
+        return False
+
+    def isattr(self):
+        """
+        Get whether the object is a schema I{attribute} definition.
+        @return: True if an attribute, else False.
+        @rtype: boolean
+        """
+        return False
+
+    def extension(self):
+        """
+        Get whether the object is an extension of another type.
+        @return: True if an extension, else False.
+        @rtype: boolean
+        """
+        return False
+
+    def restriction(self):
+        """
+        Get whether the object is an restriction of another type.
+        @return: True if an restriction, else False.
+        @rtype: boolean
+        """
+        return False
+
+    def mixed(self):
+        """
+        Get whether this I{mixed} content.
+        """
+        return False
+
+    def find(self, qref, classes=()):
+        """
+        Find a referenced type in self or children.
+        @param qref: A qualified reference.
+        @type qref: qref
+        @param classes: A list of classes used to qualify the match.
+        @type classes: [I{class},...]
+        @return: The referenced type.
+        @rtype: L{SchemaObject}
+        @see: L{qualify()}
+        """
+        if not len(classes):
+            classes = (self.__class__,)
+        if self.qname == qref and self.__class__ in classes:
+            return self
+        for c in self.rawchildren:
+            p = c.find(qref, classes)
+            if p is not None:
+                return p
+        return None
+
+    def translate(self, value, topython=True):
+        """
+        Translate a value (type) to/from a Python type.
+        @param value: A value to translate.
+        @return: The converted I{language} type.
+        """
+        return value
+
+    def childtags(self):
+        """
+        Get a list of valid child tag names.
+        @return: A list of child tag names.
+        @rtype: [str,...]
+        """
+        return ()
+
+    def dependencies(self):
+        """
+        Get a list of dependencies for dereferencing.
+        @return: A merge dependency index and a list of dependencies.
+        @rtype: (int, [L{SchemaObject},...])
+        """
+        return None, []
+
+    def autoqualified(self):
+        """
+        The list of I{auto} qualified attribute values.
+        Qualification means to convert values into I{qref}.
+        @return: A list of attibute names.
+        @rtype: list
+        """
+        return ['type', 'ref']
+
+    def qualify(self):
+        """
+        Convert attribute values, that are references to other
+        objects, into I{qref}.  Qualified using the default document namespace.
+        Since many WSDLs are written improperly: when the document does
+        not define its default namespace, the schema target namespace is used
+        to qualify references.
+        """
+        defns = self.root.defaultNamespace()
+        if Namespace.none(defns):
+            defns = self.schema.tns
+        for a in self.autoqualified():
+            ref = getattr(self, a)
+            if ref is None:
+                continue
+            if isqref(ref):
+                continue
+            qref = qualify(ref, self.root, defns)
+            log.debug('%s, convert %s="%s" to %s', self.id, a, ref, qref)
+            setattr(self, a, qref)
+
+    def merge(self, other):
+        """
+        Merge another object as needed.
+        """
+        other.qualify()
+        for n in ('name',
+                  'qname',
+                  'min',
+                  'max',
+                  'default',
+                  'type',
+                  'nillable',
+                  'form_qualified'):
+            if getattr(self, n) is not None:
+                continue
+            v = getattr(other, n)
+            if v is None:
+                continue
+            setattr(self, n, v)
+
+    def content(self, collection=None, filter=Filter(), history=None):
+        """
+        Get a I{flattened} list of this node's contents.
+        @param collection: A list to fill.
+        @type collection: list
+        @param filter: A filter used to constrain the result.
+        @type filter: L{Filter}
+        @param history: The history list used to prevent cyclic dependency.
+        @type history: list
+        @return: The filled list.
+        @rtype: list
+        """
+        if collection is None:
+            collection = []
+        if history is None:
+            history = []
+        if self in history:
+            return collection
+        history.append(self)
+        if self in filter:
+            collection.append(self)
+        for c in self.rawchildren:
+            c.content(collection, filter, history[:])
+        return collection
+
+    def str(self, indent=0, history=None):
+        """
+        Get a string representation of this object.
+        @param indent: The indent.
+        @type indent: int
+        @return: A string.
+        @rtype: str
+        """
+        if history is None:
+            history = []
+        if self in history:
+            return '%s ...' % Repr(self)
+        history.append(self)
+        tab = '%*s'%(indent*3, '')
+        result = ['%s<%s' % (tab, self.id)]
+        for n in self.description():
+            if not hasattr(self, n):
+                continue
+            v = getattr(self, n)
+            if v is None:
+                continue
+            result.append(' %s="%s"' % (n, v))
+        if len(self):
+            result.append('>')
+            for c in self.rawchildren:
+                result.append('\n')
+                result.append(c.str(indent+1, history[:]))
+                if c.isattr():
+                    result.append('@')
+            result.append('\n%s' % tab)
+            result.append('</%s>' % self.__class__.__name__)
+        else:
+            result.append(' />')
+        return ''.join(result)
+
+    def description(self):
+        """
+        Get the names used for str() and repr() description.
+        @return:  A dictionary of relevant attributes.
+        @rtype: [str,...]
+        """
+        return ()
+
+    def __unicode__(self):
+        return unicode(self.str())
+
+    def __repr__(self):
+        s = []
+        s.append('<%s' % self.id)
+        for n in self.description():
+            if not hasattr(self, n):
+                continue
+            v = getattr(self, n)
+            if v is None:
+                continue
+            s.append(' %s="%s"' % (n, v))
+        s.append(' />')
+        return ''.join(s)
+
+    def __len__(self):
+        n = 0
+        for x in self: n += 1
+        return n
+
+    def __iter__(self):
+        return Iter(self)
+
+    def __getitem__(self, index):
+        """Returns a contained schema object referenced by its 0-based index.
+
+        Returns None if such an object does not exist.
+
+        """
+        i = 0
+        for c in self:
+            if i == index:
+                return c
+            i += 1
+
+
+class Iter:
+    """
+    The content iterator - used to iterate the L{Content} children.  The
+    iterator provides a I{view} of the children that is free of container
+    elements such as <sequence/> and <choice/>.
+    @ivar stack: A stack used to control nesting.
+    @type stack: list
+    """
+
+    class Frame:
+        """ A content iterator frame. """
+
+        def __init__(self, sx):
+            """
+            @param sx: A schema object.
+            @type sx: L{SchemaObject}
+            """
+            self.sx = sx
+            self.items = sx.rawchildren
+            self.index = 0
+
+        def next(self):
+            """
+            Get the I{next} item in the frame's collection.
+            @return: The next item or None
+            @rtype: L{SchemaObject}
+            """
+            if self.index < len(self.items):
+                result = self.items[self.index]
+                self.index += 1
+                return result
+
+    def __init__(self, sx):
+        """
+        @param sx: A schema object.
+        @type sx: L{SchemaObject}
+        """
+        self.stack = []
+        self.push(sx)
+
+    def push(self, sx):
+        """
+        Create a frame and push the specified object.
+        @param sx: A schema object to push.
+        @type sx: L{SchemaObject}
+        """
+        self.stack.append(Iter.Frame(sx))
+
+    def pop(self):
+        """
+        Pop the I{top} frame.
+        @return: The popped frame.
+        @rtype: L{Frame}
+        @raise StopIteration: when stack is empty.
+        """
+        if len(self.stack):
+            return self.stack.pop()
+        else:
+            raise StopIteration()
+
+    def top(self):
+        """
+        Get the I{top} frame.
+        @return: The top frame.
+        @rtype: L{Frame}
+        @raise StopIteration: when stack is empty.
+        """
+        if len(self.stack):
+            return self.stack[-1]
+        else:
+            raise StopIteration()
+
+    def next(self):
+        """
+        Get the next item.
+        @return: A tuple: the next (child, ancestry).
+        @rtype: (L{SchemaObject}, [L{SchemaObject},..])
+        @raise StopIteration: A the end.
+        """
+        frame = self.top()
+        while True:
+            result = frame.next()
+            if result is None:
+                self.pop()
+                return self.next()
+            if isinstance(result, Content):
+                ancestry = [f.sx for f in self.stack]
+                return result, ancestry
+            self.push(result)
+            return self.next()
+
+    def __iter__(self):
+        return self
+
+
+class XBuiltin(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:*/> node.
+    """
+
+    def __init__(self, schema, name):
+        """
+        @param schema: The containing schema.
+        @type schema: L{schema.Schema}
+        """
+        root = Element(name)
+        SchemaObject.__init__(self, schema, root)
+        self.name = name
+        self.nillable = True
+
+    def namespace(self, prefix=None):
+        return Namespace.xsdns
+
+    def builtin(self):
+        return True
+
+
+class Content(SchemaObject):
+    """
+    This class represents those schema objects that represent
+    real XML document content.
+    """
+    pass
+
+
+class NodeFinder:
+    """
+    Find nodes based on flexable criteria.  The I{matcher}
+    may be any object that implements a match(n) method.
+    @ivar matcher: An object used as criteria for match.
+    @type matcher: I{any}.match(n)
+    @ivar limit: Limit the number of matches.  0=unlimited.
+    @type limit: int
+    """
+    def __init__(self, matcher, limit=0):
+        """
+        @param matcher: An object used as criteria for match.
+        @type matcher: I{any}.match(n)
+        @param limit: Limit the number of matches.  0=unlimited.
+        @type limit: int
+        """
+        self.matcher = matcher
+        self.limit = limit
+
+    def find(self, node, list):
+        """
+        Traverse the tree looking for matches.
+        @param node: A node to match on.
+        @type node: L{SchemaObject}
+        @param list: A list to fill.
+        @type list: list
+        """
+        if self.matcher.match(node):
+            list.append(node)
+            self.limit -= 1
+            if self.limit == 0:
+                return
+        for c in node.rawchildren:
+            self.find(c, list)
+        return self
diff --git a/suds/xsd/sxbasic.py b/suds/xsd/sxbasic.py
new file mode 100644
index 00000000..f6050885
--- /dev/null
+++ b/suds/xsd/sxbasic.py
@@ -0,0 +1,857 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{sxbasic} module provides classes that represent
+I{basic} schema objects.
+"""
+
+from suds import *
+from suds.xsd import *
+from suds.xsd.sxbase import *
+from suds.xsd.query import *
+from suds.sax import Namespace
+from suds.transport import TransportError
+from suds.reader import DocumentReader
+from urlparse import urljoin
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+class RestrictionMatcher:
+    """
+    For use with L{NodeFinder} to match restriction.
+    """
+    def match(self, n):
+        return isinstance(n, Restriction)
+
+
+class TypedContent(Content):
+    """
+    Represents any I{typed} content.
+    """
+
+    def __init__(self, *args, **kwargs):
+        Content.__init__(self, *args, **kwargs)
+        self.resolved_cache = {}
+
+    def resolve(self, nobuiltin=False):
+        """
+        Resolve the node's type reference and return the referenced type node.
+
+        Returns self if the type is defined locally, e.g. as a <complexType>
+        subnode. Otherwise returns the referenced external node.
+        @param nobuiltin: Flag indicating whether resolving to XSD builtin
+            types should not be allowed.
+        @return: The resolved (true) type.
+        @rtype: L{SchemaObject}
+        """
+        cached = self.resolved_cache.get(nobuiltin)
+        if cached is not None:
+            return cached
+        resolved = self.__resolve_type(nobuiltin)
+        self.resolved_cache[nobuiltin] = resolved
+        return resolved
+
+    def __resolve_type(self, nobuiltin=False):
+        """
+        Private resolve() worker without any result caching.
+        @param nobuiltin: Flag indicating whether resolving to XSD builtin
+            types should not be allowed.
+        @return: The resolved (true) type.
+        @rtype: L{SchemaObject}
+
+        Implementation note:
+          Note that there is no need for a recursive implementation here since
+        a node can reference an external type node but there is no way using
+        WSDL to then make that type node actually be a reference to a different
+        type node.
+        """
+        qref = self.qref()
+        if qref is None:
+            return self
+        query = TypeQuery(qref)
+        query.history = [self]
+        log.debug('%s, resolving: %s\n using:%s', self.id, qref, query)
+        resolved = query.execute(self.schema)
+        if resolved is None:
+            log.debug(self.schema)
+            raise TypeNotFound(qref)
+        if resolved.builtin() and nobuiltin:
+            return self
+        return resolved
+
+    def qref(self):
+        """
+        Get the I{type} qualified reference to the referenced XSD type.
+        This method takes into account simple types defined through
+        restriction which are detected by determining that self is simple
+        (len=0) and by finding a restriction child.
+        @return: The I{type} qualified reference.
+        @rtype: qref
+        """
+        qref = self.type
+        if qref is None and len(self) == 0:
+            ls = []
+            m = RestrictionMatcher()
+            finder = NodeFinder(m, 1)
+            finder.find(self, ls)
+            if len(ls):
+                return ls[0].ref
+        return qref
+
+
+class Complex(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:complexType/> node.
+    @cvar childtags: A list of valid child node names.
+    @type childtags: (I{str},...)
+    """
+
+    def childtags(self):
+        return 'attribute', 'attributeGroup', 'sequence', 'all', 'choice',  \
+            'complexContent', 'simpleContent', 'any', 'group'
+
+    def description(self):
+        return ('name',)
+
+    def extension(self):
+        for c in self.rawchildren:
+            if c.extension():
+                return True
+        return False
+
+    def mixed(self):
+        for c in self.rawchildren:
+            if isinstance(c, SimpleContent) and c.mixed():
+                return True
+        return False
+
+
+class Group(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:group/> node.
+    @cvar childtags: A list of valid child node names.
+    @type childtags: (I{str},...)
+    """
+
+    def childtags(self):
+        return 'sequence', 'all', 'choice'
+
+    def dependencies(self):
+        deps = []
+        midx = None
+        if self.ref is not None:
+            query = GroupQuery(self.ref)
+            g = query.execute(self.schema)
+            if g is None:
+                log.debug(self.schema)
+                raise TypeNotFound(self.ref)
+            deps.append(g)
+            midx = 0
+        return midx, deps
+
+    def merge(self, other):
+        SchemaObject.merge(self, other)
+        self.rawchildren = other.rawchildren
+
+    def description(self):
+        return 'name', 'ref'
+
+
+class AttributeGroup(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:attributeGroup/> node.
+    @cvar childtags: A list of valid child node names.
+    @type childtags: (I{str},...)
+    """
+
+    def childtags(self):
+        return 'attribute', 'attributeGroup'
+
+    def dependencies(self):
+        deps = []
+        midx = None
+        if self.ref is not None:
+            query = AttrGroupQuery(self.ref)
+            ag = query.execute(self.schema)
+            if ag is None:
+                log.debug(self.schema)
+                raise TypeNotFound(self.ref)
+            deps.append(ag)
+            midx = 0
+        return midx, deps
+
+    def merge(self, other):
+        SchemaObject.merge(self, other)
+        self.rawchildren = other.rawchildren
+
+    def description(self):
+        return 'name', 'ref'
+
+
+class Simple(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:simpleType/> node.
+    """
+
+    def childtags(self):
+        return 'restriction', 'any', 'list'
+
+    def enum(self):
+        for child, ancestry in self.children():
+            if isinstance(child, Enumeration):
+                return True
+        return False
+
+    def mixed(self):
+        return len(self)
+
+    def description(self):
+        return ('name',)
+
+    def extension(self):
+        for c in self.rawchildren:
+            if c.extension():
+                return True
+        return False
+
+    def restriction(self):
+        for c in self.rawchildren:
+            if c.restriction():
+                return True
+        return False
+
+
+class List(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:list/> node.
+    """
+
+    def childtags(self):
+        return ()
+
+    def description(self):
+        return ('name',)
+
+    def xslist(self):
+        return True
+
+
+class Restriction(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:restriction/> node.
+    """
+
+    def __init__(self, schema, root):
+        SchemaObject.__init__(self, schema, root)
+        self.ref = root.get('base')
+
+    def childtags(self):
+        return 'enumeration', 'attribute', 'attributeGroup'
+
+    def dependencies(self):
+        deps = []
+        midx = None
+        if self.ref is not None:
+            query = TypeQuery(self.ref)
+            super = query.execute(self.schema)
+            if super is None:
+                log.debug(self.schema)
+                raise TypeNotFound(self.ref)
+            if not super.builtin():
+                deps.append(super)
+                midx = 0
+        return midx, deps
+
+    def restriction(self):
+        return True
+
+    def merge(self, other):
+        SchemaObject.merge(self, other)
+        filter = Filter(False, self.rawchildren)
+        self.prepend(self.rawchildren, other.rawchildren, filter)
+
+    def description(self):
+        return ('ref',)
+
+
+class Collection(SchemaObject):
+    """
+    Represents an (XSD) schema collection node:
+        - sequence
+        - choice
+        - all
+    """
+
+    def childtags(self):
+        return 'element', 'sequence', 'all', 'choice', 'any', 'group'
+
+
+class Sequence(Collection):
+    """
+    Represents an (XSD) schema <xs:sequence/> node.
+    """
+    def sequence(self):
+        return True
+
+
+class All(Collection):
+    """
+    Represents an (XSD) schema <xs:all/> node.
+    """
+    def all(self):
+        return True
+
+
+class Choice(Collection):
+    """
+    Represents an (XSD) schema <xs:choice/> node.
+    """
+    def choice(self):
+        return True
+
+
+class ComplexContent(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:complexContent/> node.
+    """
+
+    def childtags(self):
+        return 'attribute', 'attributeGroup', 'extension', 'restriction'
+
+    def extension(self):
+        for c in self.rawchildren:
+            if c.extension():
+                return True
+        return False
+
+    def restriction(self):
+        for c in self.rawchildren:
+            if c.restriction():
+                return True
+        return False
+
+
+class SimpleContent(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:simpleContent/> node.
+    """
+
+    def childtags(self):
+        return 'extension', 'restriction'
+
+    def extension(self):
+        for c in self.rawchildren:
+            if c.extension():
+                return True
+        return False
+
+    def restriction(self):
+        for c in self.rawchildren:
+            if c.restriction():
+                return True
+        return False
+
+    def mixed(self):
+        return len(self)
+
+
+class Enumeration(Content):
+    """
+    Represents an (XSD) schema <xs:enumeration/> node.
+    """
+
+    def __init__(self, schema, root):
+        Content.__init__(self, schema, root)
+        self.name = root.get('value')
+
+    def description(self):
+        return ('name',)
+
+    def enum(self):
+        return True
+
+
+class Element(TypedContent):
+    """
+    Represents an (XSD) schema <xs:element/> node.
+    """
+
+    def __init__(self, schema, root):
+        TypedContent.__init__(self, schema, root)
+        a = root.get('form')
+        if a is not None:
+            self.form_qualified = ( a == 'qualified' )
+        a = self.root.get('nillable')
+        if a is not None:
+            self.nillable = ( a in ('1', 'true') )
+        self.implany()
+
+    def implany(self):
+        """
+        Set the type as any when implicit.
+        An implicit <xs:any/> is when an element has not
+        body and no type defined.
+        @return: self
+        @rtype: L{Element}
+        """
+        if self.type is None and \
+            self.ref is None and \
+            self.root.isempty():
+                self.type = self.anytype()
+        return self
+
+    def childtags(self):
+        return 'attribute', 'simpleType', 'complexType', 'any'
+
+    def extension(self):
+        for c in self.rawchildren:
+            if c.extension():
+                return True
+        return False
+
+    def restriction(self):
+        for c in self.rawchildren:
+            if c.restriction():
+                return True
+        return False
+
+    def dependencies(self):
+        deps = []
+        midx = None
+        e = self.__deref()
+        if e is not None:
+            deps.append(e)
+            midx = 0
+        return midx, deps
+
+    def merge(self, other):
+        SchemaObject.merge(self, other)
+        self.rawchildren = other.rawchildren
+
+    def description(self):
+        return 'name', 'ref', 'type'
+
+    def anytype(self):
+        """ create an xsd:anyType reference """
+        p, u = Namespace.xsdns
+        mp = self.root.findPrefix(u)
+        if mp is None:
+            mp = p
+            self.root.addPrefix(p, u)
+        return ':'.join((mp, 'anyType'))
+
+    def namespace(self, prefix=None):
+        """
+        Get this schema element's target namespace.
+
+        In case of reference elements, the target namespace is defined by the
+        referenced and not the referencing element node.
+
+        @param prefix: The default prefix.
+        @type prefix: str
+        @return: The schema element's target namespace
+        @rtype: (I{prefix},I{URI})
+        """
+        e = self.__deref()
+        if e is not None:
+            return e.namespace(prefix)
+        return super(Element, self).namespace()
+
+    def __deref(self):
+        if self.ref is None:
+            return
+        query = ElementQuery(self.ref)
+        e = query.execute(self.schema)
+        if e is None:
+            log.debug(self.schema)
+            raise TypeNotFound(self.ref)
+        return e
+
+
+class Extension(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:extension/> node.
+    """
+
+    def __init__(self, schema, root):
+        SchemaObject.__init__(self, schema, root)
+        self.ref = root.get('base')
+
+    def childtags(self):
+        return 'attribute', 'attributeGroup', 'sequence', 'all', 'choice',  \
+            'group'
+
+    def dependencies(self):
+        deps = []
+        midx = None
+        if self.ref is not None:
+            query = TypeQuery(self.ref)
+            super = query.execute(self.schema)
+            if super is None:
+                log.debug(self.schema)
+                raise TypeNotFound(self.ref)
+            if not super.builtin():
+                deps.append(super)
+                midx = 0
+        return midx, deps
+
+    def merge(self, other):
+        SchemaObject.merge(self, other)
+        filter = Filter(False, self.rawchildren)
+        self.prepend(self.rawchildren, other.rawchildren, filter)
+
+    def extension(self):
+        return self.ref is not None
+
+    def description(self):
+        return ('ref',)
+
+
+class Import(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:import/> node.
+    @cvar locations: A dictionary of namespace locations.
+    @type locations: dict
+    @ivar ns: The imported namespace.
+    @type ns: str
+    @ivar location: The (optional) location.
+    @type location: namespace-uri
+    @ivar opened: Opened and I{imported} flag.
+    @type opened: boolean
+    """
+
+    locations = {}
+
+    @classmethod
+    def bind(cls, ns, location=None):
+        """
+        Bind a namespace to a schema location (URI).
+        This is used for imports that don't specify a schemaLocation.
+        @param ns: A namespace-uri.
+        @type ns: str
+        @param location: The (optional) schema location for the
+            namespace.  (default=ns).
+        @type location: str
+        """
+        if location is None:
+            location = ns
+        cls.locations[ns] = location
+
+    def __init__(self, schema, root):
+        SchemaObject.__init__(self, schema, root)
+        self.ns = (None, root.get('namespace'))
+        self.location = root.get('schemaLocation')
+        if self.location is None:
+            self.location = self.locations.get(self.ns[1])
+        self.opened = False
+
+    def open(self, options):
+        """
+        Open and import the refrenced schema.
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        @return: The referenced schema.
+        @rtype: L{Schema}
+        """
+        if self.opened:
+            return
+        self.opened = True
+        log.debug('%s, importing ns="%s", location="%s"', self.id, self.ns[1], self.location)
+        result = self.locate()
+        if result is None:
+            if self.location is None:
+                log.debug('imported schema (%s) not-found', self.ns[1])
+            else:
+                result = self.download(options)
+        log.debug('imported:\n%s', result)
+        return result
+
+    def locate(self):
+        """ find the schema locally """
+        if self.ns[1] != self.schema.tns[1]:
+            return self.schema.locate(self.ns)
+
+    def download(self, options):
+        """ download the schema """
+        url = self.location
+        try:
+            if '://' not in url:
+                url = urljoin(self.schema.baseurl, url)
+            reader = DocumentReader(options)
+            d = reader.open(url)
+            root = d.root()
+            root.set('url', url)
+            return self.schema.instance(root, url, options)
+        except TransportError:
+            msg = 'imported schema (%s) at (%s), failed' % (self.ns[1], url)
+            log.error('%s, %s', self.id, msg, exc_info=True)
+            raise Exception(msg)
+
+    def description(self):
+        return 'ns', 'location'
+
+
+class Include(SchemaObject):
+    """
+    Represents an (XSD) schema <xs:include/> node.
+    @ivar location: The (optional) location.
+    @type location: namespace-uri
+    @ivar opened: Opened and I{imported} flag.
+    @type opened: boolean
+    """
+
+    locations = {}
+
+    def __init__(self, schema, root):
+        SchemaObject.__init__(self, schema, root)
+        self.location = root.get('schemaLocation')
+        if self.location is None:
+            self.location = self.locations.get(self.ns[1])
+        self.opened = False
+
+    def open(self, options):
+        """
+        Open and include the refrenced schema.
+        @param options: An options dictionary.
+        @type options: L{options.Options}
+        @return: The referenced schema.
+        @rtype: L{Schema}
+        """
+        if self.opened:
+            return
+        self.opened = True
+        log.debug('%s, including location="%s"', self.id, self.location)
+        result = self.download(options)
+        log.debug('included:\n%s', result)
+        return result
+
+    def download(self, options):
+        """ download the schema """
+        url = self.location
+        try:
+            if '://' not in url:
+                url = urljoin(self.schema.baseurl, url)
+            reader = DocumentReader(options)
+            d = reader.open(url)
+            root = d.root()
+            root.set('url', url)
+            self.__applytns(root)
+            return self.schema.instance(root, url, options)
+        except TransportError:
+            msg = 'include schema at (%s), failed' % url
+            log.error('%s, %s', self.id, msg, exc_info=True)
+            raise Exception(msg)
+
+    def __applytns(self, root):
+        """ make sure included schema has same tns. """
+        TNS = 'targetNamespace'
+        tns = root.get(TNS)
+        if tns is None:
+            tns = self.schema.tns[1]
+            root.set(TNS, tns)
+        else:
+            if self.schema.tns[1] != tns:
+                raise Exception, '%s mismatch' % TNS
+
+
+    def description(self):
+        return 'location'
+
+
+class Attribute(TypedContent):
+    """
+    Represents an (XSD) <attribute/> node.
+    """
+
+    def __init__(self, schema, root):
+        TypedContent.__init__(self, schema, root)
+        self.use = root.get('use', default='')
+
+    def childtags(self):
+        return ('restriction',)
+
+    def isattr(self):
+        return True
+
+    def get_default(self):
+        """
+        Gets the <xs:attribute default=""/> attribute value.
+        @return: The default value for the attribute
+        @rtype: str
+        """
+        return self.root.get('default', default='')
+
+    def optional(self):
+        return self.use != 'required'
+
+    def dependencies(self):
+        deps = []
+        midx = None
+        if self.ref is not None:
+            query = AttrQuery(self.ref)
+            a = query.execute(self.schema)
+            if a is None:
+                log.debug(self.schema)
+                raise TypeNotFound(self.ref)
+            deps.append(a)
+            midx = 0
+        return midx, deps
+
+    def description(self):
+        return 'name', 'ref', 'type'
+
+
+class Any(Content):
+    """
+    Represents an (XSD) <any/> node.
+    """
+
+    def get_child(self, name):
+        root = self.root.clone()
+        root.set('note', 'synthesized (any) child')
+        child = Any(self.schema, root)
+        return child, []
+
+    def get_attribute(self, name):
+        root = self.root.clone()
+        root.set('note', 'synthesized (any) attribute')
+        attribute = Any(self.schema, root)
+        return attribute, []
+
+    def any(self):
+        return True
+
+
+class Factory:
+    """
+    @cvar tags: A factory to create object objects based on tag.
+    @type tags: {tag:fn,}
+    """
+
+    tags = {
+        'import' : Import,
+        'include' : Include,
+        'complexType' : Complex,
+        'group' : Group,
+        'attributeGroup' : AttributeGroup,
+        'simpleType' : Simple,
+        'list' : List,
+        'element' : Element,
+        'attribute' : Attribute,
+        'sequence' : Sequence,
+        'all' : All,
+        'choice' : Choice,
+        'complexContent' : ComplexContent,
+        'simpleContent' : SimpleContent,
+        'restriction' : Restriction,
+        'enumeration' : Enumeration,
+        'extension' : Extension,
+        'any' : Any,
+    }
+
+    @classmethod
+    def maptag(cls, tag, fn):
+        """
+        Map (override) tag => I{class} mapping.
+        @param tag: An XSD tag name.
+        @type tag: str
+        @param fn: A function or class.
+        @type fn: fn|class.
+        """
+        cls.tags[tag] = fn
+
+    @classmethod
+    def create(cls, root, schema):
+        """
+        Create an object based on the root tag name.
+        @param root: An XML root element.
+        @type root: L{Element}
+        @param schema: A schema object.
+        @type schema: L{schema.Schema}
+        @return: The created object.
+        @rtype: L{SchemaObject}
+        """
+        fn = cls.tags.get(root.name)
+        if fn is not None:
+            return fn(schema, root)
+
+    @classmethod
+    def build(cls, root, schema, filter=('*',)):
+        """
+        Build an xsobject representation.
+        @param root: An schema XML root.
+        @type root: L{sax.element.Element}
+        @param filter: A tag filter.
+        @type filter: [str,...]
+        @return: A schema object graph.
+        @rtype: L{sxbase.SchemaObject}
+        """
+        children = []
+        for node in root.getChildren(ns=Namespace.xsdns):
+            if '*' in filter or node.name in filter:
+                child = cls.create(node, schema)
+                if child is None:
+                    continue
+                children.append(child)
+                c = cls.build(node, schema, child.childtags())
+                child.rawchildren = c
+        return children
+
+    @classmethod
+    def collate(cls, children):
+        imports = []
+        elements = {}
+        attributes = {}
+        types = {}
+        groups = {}
+        agrps = {}
+        for c in children:
+            if isinstance(c, (Import, Include)):
+                imports.append(c)
+                continue
+            if isinstance(c, Attribute):
+                attributes[c.qname] = c
+                continue
+            if isinstance(c, Element):
+                elements[c.qname] = c
+                continue
+            if isinstance(c, Group):
+                groups[c.qname] = c
+                continue
+            if isinstance(c, AttributeGroup):
+                agrps[c.qname] = c
+                continue
+            types[c.qname] = c
+        for i in imports:
+            children.remove(i)
+        return children, imports, attributes, elements, types, groups, agrps
+
+
+#######################################################
+# Static Import Bindings :-(
+#######################################################
+Import.bind(
+    'http://schemas.xmlsoap.org/soap/encoding/',
+    'suds://schemas.xmlsoap.org/soap/encoding/')
+Import.bind(
+    'http://www.w3.org/XML/1998/namespace',
+    'http://www.w3.org/2001/xml.xsd')
+Import.bind(
+    'http://www.w3.org/2001/XMLSchema',
+    'http://www.w3.org/2001/XMLSchema.xsd')
diff --git a/suds/xsd/sxbuiltin.py b/suds/xsd/sxbuiltin.py
new file mode 100644
index 00000000..3d2c3dde
--- /dev/null
+++ b/suds/xsd/sxbuiltin.py
@@ -0,0 +1,255 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel@redhat.com )
+
+"""
+The I{sxbuiltin} module provides classes that represent
+XSD I{builtin} schema objects.
+"""
+
+from suds import *
+from suds.xsd import *
+from suds.sax.date import *
+from suds.xsd.sxbase import XBuiltin
+
+import datetime as dt
+
+
+class XString(XBuiltin):
+    """
+    Represents an (xsd) <xs:string/> node
+    """
+    pass
+
+
+class XAny(XBuiltin):
+    """
+    Represents an (xsd) <any/> node
+    """
+
+    def __init__(self, schema, name):
+        XBuiltin.__init__(self, schema, name)
+        self.nillable = False
+
+    def get_child(self, name):
+        child = XAny(self.schema, name)
+        return child, []
+
+    def any(self):
+        return True
+
+
+class XBoolean(XBuiltin):
+    """
+    Represents an (xsd) boolean builtin type.
+    """
+
+    translation = ({'1':True, 'true':True, '0':False, 'false':False},
+        {True:'true', 1:'true', False:'false', 0:'false'})
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring):
+                return XBoolean.translation[0].get(value)
+        else:
+            if isinstance(value, (bool, int)):
+                return XBoolean.translation[1].get(value)
+            return value
+
+
+class XInteger(XBuiltin):
+    """
+    Represents an (xsd) xs:int builtin type.
+    """
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and len(value):
+                return int(value)
+        else:
+            if isinstance(value, int):
+                return str(value)
+            return value
+
+
+class XLong(XBuiltin):
+    """
+    Represents an (xsd) xs:long builtin type.
+    """
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and len(value):
+                return long(value)
+        else:
+            if isinstance(value, (int, long)):
+                return str(value)
+            return value
+
+
+class XFloat(XBuiltin):
+    """
+    Represents an (xsd) xs:float builtin type.
+    """
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and len(value):
+                return float(value)
+        else:
+            if isinstance(value, float):
+                return str(value)
+            return value
+
+
+class XDate(XBuiltin):
+    """
+    Represents an (xsd) xs:date builtin type.
+    """
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and len(value):
+                return Date(value).value
+        else:
+            if isinstance(value, dt.date):
+                return str(Date(value))
+            return value
+
+
+class XTime(XBuiltin):
+    """
+    Represents an (xsd) xs:time builtin type.
+    """
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and len(value):
+                return Time(value).value
+        else:
+            if isinstance(value, dt.time):
+                return str(Time(value))
+            return value
+
+
+class XDateTime(XBuiltin):
+    """
+    Represents an (xsd) xs:datetime builtin type.
+    """
+
+    @staticmethod
+    def translate(value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and len(value):
+                return DateTime(value).value
+        else:
+            if isinstance(value, dt.datetime):
+                return str(DateTime(value))
+            return value
+
+
+class Factory:
+
+    tags =\
+    {
+        # any
+        'anyType' : XAny,
+        # strings
+        'string' : XString,
+        'normalizedString' : XString,
+        'ID' : XString,
+        'Name' : XString,
+        'QName' : XString,
+        'NCName' : XString,
+        'anySimpleType' : XString,
+        'anyURI' : XString,
+        'NOTATION' : XString,
+        'token' : XString,
+        'language' : XString,
+        'IDREFS' : XString,
+        'ENTITIES' : XString,
+        'IDREF' : XString,
+        'ENTITY' : XString,
+        'NMTOKEN' : XString,
+        'NMTOKENS' : XString,
+        # binary
+        'hexBinary' : XString,
+        'base64Binary' : XString,
+        # integers
+        'int' : XInteger,
+        'integer' : XInteger,
+        'unsignedInt' : XInteger,
+        'positiveInteger' : XInteger,
+        'negativeInteger' : XInteger,
+        'nonPositiveInteger' : XInteger,
+        'nonNegativeInteger' : XInteger,
+        # longs
+        'long' : XLong,
+        'unsignedLong' : XLong,
+        # shorts
+        'short' : XInteger,
+        'unsignedShort' : XInteger,
+        'byte' : XInteger,
+        'unsignedByte' : XInteger,
+        # floats
+        'float' : XFloat,
+        'double' : XFloat,
+        'decimal' : XFloat,
+        # dates & times
+        'date' : XDate,
+        'time' : XTime,
+        'dateTime': XDateTime,
+        'duration': XString,
+        'gYearMonth' : XString,
+        'gYear' : XString,
+        'gMonthDay' : XString,
+        'gDay' : XString,
+        'gMonth' : XString,
+        # boolean
+        'boolean' : XBoolean,
+    }
+
+    @classmethod
+    def maptag(cls, tag, fn):
+        """
+        Map (override) tag => I{class} mapping.
+        @param tag: An xsd tag name.
+        @type tag: str
+        @param fn: A function or class.
+        @type fn: fn|class.
+        """
+        cls.tags[tag] = fn
+
+    @classmethod
+    def create(cls, schema, name):
+        """
+        Create an object based on the root tag name.
+        @param schema: A schema object.
+        @type schema: L{schema.Schema}
+        @param name: The name.
+        @type name: str
+        @return: The created object.
+        @rtype: L{XBuiltin}
+        """
+        fn = cls.tags.get(name)
+        if fn is not None:
+            return fn(schema, name)
+        return XBuiltin(schema, name)
-- 
GitLab