[tryton-debian-vcs] python-zeep branch upstream created. 29d363182f3c39e10cc157475da1ef02c63d4095

Mathias Behrle tryton-debian-vcs at alioth.debian.org
Wed Dec 7 17:48:04 UTC 2016


The following commit has been merged in the upstream branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/python-zeep.git;a=commitdiff;h=29d363182f3c39e10cc157475da1ef02c63d4095
commit 29d363182f3c39e10cc157475da1ef02c63d4095
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Tue Dec 6 17:32:03 2016 +0100

    Adding upstream version 0.23.
    
    Signed-off-by: Mathias Behrle <mathiasb at m9s.biz>

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4926c2f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+root = true
+
+[*.py]
+line_length = 79
+multi_line_output = 4
+balanced_wrapping = true
+known_first_party = zeep,tests
+use_parentheses = true
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.yml]
+indent_size = 2
+shift_width = 2
+
+[Makefile]
+indent_style = tab
+indent_size = 4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..166db7a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+*.egg-info
+*.pyc
+.tox
+.coverage
+.eggs
+.cache
+.python-version
+.venv
+.idea/
+/build/
+/dist/
+/test_clients/
+/docs/_build/
+/frutsels/
+/server/
+/htmlcov/
+
+
+# Editors
+.idea/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..d2de223
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,40 @@
+---
+sudo: false
+language: python
+
+python:
+  - '2.7'
+  - '3.3'
+  - '3.4'
+  - '3.5'
+  - 'pypy'
+
+install:
+  - |
+      if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
+        export PYENV_ROOT="$HOME/.pyenv"
+        if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
+          pushd "$PYENV_ROOT" && git pull && popd
+        else
+          rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
+        fi
+        export PYPY_VERSION="5.4"
+        "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION"
+        virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
+        source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
+      fi
+  - pip install codecov
+  - pip install -e .[test]
+
+script:
+  - py.test --cov=zeep --cov-report=term-missing
+
+after_success:
+  - codecov
+
+before_cache:
+  - rm -rf $HOME/.cache/pip/log
+
+cache:
+  directories:
+    - $HOME/.cache/pip
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..468f461
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,355 @@
+0.23.0 (2016-11-24)
+-------------------
+ - Add Client.set_default_soapheaders() to set soapheaders which are to be used
+   on all operations done via the client object.
+ - Add basic support for asyncio using aiohttp. Many thanks to chrisimcevoy
+   for the initial implementation! Please see 
+   https://github.com/mvantellingen/python-zeep/pull/207 and
+   https://github.com/mvantellingen/python-zeep/pull/251 for more information
+ - Fix recursion error when generating the call signature (jaceksnet, #264)
+
+
+0.22.1 (2016-11-22)
+-------------------
+ - Fix reversed() error (jaceksnet) (#260)
+ - Better error message when unexpected xml elements are encountered in 
+   sequences.
+
+
+0.22.0 (2016-11-13)
+-------------------
+ - Force the soap:address / http:address to HTTPS when the wsdl is loaded from
+   a https url (#228)
+ - Improvements to the xsd:union handling. The matching base class is now used
+   for serializing/deserializing the values. If there is no matching base class 
+   then the raw value is returned. (#195)
+ - Fix handling of xsd:any with maxOccurs > 1 in xsd:choice elements (#253)
+ - Add workaround for schema's importing the xsd from 
+   http://www.w3.org/XML/1998/namespace (#220)
+ - Add new Client.type_factory(namespace) method which returns a factory to
+   simplify creation of types. 
+
+
+
+0.21.0 (2016-11-02)
+-------------------
+ - Don't error on empty xml namespaces declarations in inline schema's (#186)
+ - Wrap importing of sqlite3 in try..except for Google App Engine (#243)
+ - Don't use pkg_resources to determine the zeep version, use __version__ 
+   instead (#243).
+ - Fix SOAP arrays by wrapping children in the appropriate element 
+   (joeribekker, #236)
+ - Add ``operation_timeout`` kwarg to the Transport class to set timeouts for
+   operations. The default is still no timeout (#140)
+ - Introduce client.options context manager to temporarily override various
+   options (only timeout for now) (#140)
+ - Wrap the parsing of xml values in a try..except block and log an error 
+   instead of throwing an exception (#137)
+ - Fix xsd:choice xml rendering with nested choice/sequence structure (#221)
+ - Correctly resolve header elements of which the message part defines the
+   type instead of element. (#199)
+
+
+0.20.0 (2016-10-24)
+-------------------
+ - Major performance improvements / lower memory usage. Zeep now no longer
+   copies data and alters it in place but instead uses a set to keep track of 
+   modified data.
+ - Fix parsing empty soap response (#223)
+ - Major refactor of the xsd:extension / xsd:restriction implementation.
+ - Better support for xsd:anyType, by re-using the xsd.AnyObject (#229)
+ - Deserialize SOAP response without message elements correctly (#237)
+
+
+0.19.0 (2016-10-18)
+-------------------
+ - **backwards-incompatible**: If the WSDL defines that the endpoint returns
+   soap:header elements and/or multple soap:body messages then the return
+   signature of the operation is changed. You can now explcitly access the 
+   body and header elements.
+ - Fix parsing HTTP bindings when there are no message elements (#185)
+ - Fix deserializing RPC responses (#219
+ - Add support for SOAP 1.2 Fault subcodes (#210, vashek)
+ - Don't alter the _soapheaders elements during rendering, instead create a 
+   deepcopy first. (#188)
+ - Add the SOAPAction to the Content-Type header in SOAP 1.2 bindings (#211)
+ - Fix issue when mixing elements and any elements in a choice type (#192)
+ - Improving parsing of results for union types (#192)
+ - Make ws-addressing work with lxml < 3.5 (#209)
+ - Fix recursion error when xsi:type='anyType' is given. (#198)
+
+
+0.18.1 (2016-09-23)
+-------------------
+ - PyPi release error
+
+
+0.18.0 (2016-09-23)
+-------------------
+ - Fix parsing Any elements by using the namespace map of the response node 
+   instead of the namespace map of the wsdl. (#184, #164)
+ - Improve handling of nested choice elements (choice>sequence>choice)
+
+
+0.17.0 (2016-09-12)
+-------------------
+ - Add support for xsd:notation (#183)
+ - Add improvements to resolving phase so that all objects are resolved.
+ - Improve implementation of xsd.attributeGroup and xsd.UniqueType
+ - Create a deepcopy of the args and kwargs passed to objects so that the 
+   original are unmodified.
+ - Improve handling of wsdl:arrayType
+
+
+0.16.0 (2016-09-06)
+-------------------
+ - Fix error when rendering choice elements with have sequences as children, 
+   see #150
+ - Re-use credentials passed to python -mzeep <wsdl> (#130)
+ - Workaround invalid usage of qualified vs non-qualified element tags in the
+   response handling (#176)
+ - Fix regression when importing xsd:schema's via wsdl:import statements (#179)
+
+
+0.15.0 (2016-09-04)
+-------------------
+ - All wsdl documents and xsd schemas are now globally available for eachother.
+   While this is not correct according to the (messy) soap specifications, it
+   does make zeep more compatible with all the invalid wsdl documents out
+   there. (#159)
+ - Implement support for attributeGroup (#160)
+ - Add experimental support for ws-addressing (#92)
+ - Fix handling of Mime messages with no parts (#168)
+ - Workaround an issue where soap servers don't qualify references (#170)
+ - Correctly process attributes which are passed as a dictionary. (#125)
+ - Add support for plugins, see documentation for examples.
+ - Fix helpers.serialize_object for lists of objects (#123).
+ - Add HistoryPlugin which ofers last_sent and last_received properties (#93).
+
+
+0.14.0 (2016-08-03)
+-------------------
+ - Global attributes are now always correctly handled as qualified. (#129)
+ - Fix parsing xml data containing simpleContent types (#136). 
+ - Set xsi:nil attribute when serializing objects to xml (#141)
+ - Fix rendering choice elements when the element is mixed with other elements
+   in a sequence (#150)
+ - Fix maximum recursion error for recursive xsd:include elements
+ - Make wsdl:import statements transitive. (#149)
+ - Merge xsd:schema's which are spread around imported wsdl objects. (#146)
+ - Don't raise exception when no value is given for AnyAttribute (#152)
+
+
+0.13.0 (2016-07-17)
+-------------------
+ - Use warnings.warn() for duplicate target namespaces instead of raising an
+   exception. This better matches with what lxml does.
+ - **backwards-incompatible**: The ``persistent`` kwarg is removed from the
+   SqliteCache.__init__() call. Use the new InMemoryCache() instead when you
+   don't want to persist data. This was required to make the SqliteCache 
+   backend thread-safe since we now open/close the db when writing/reading 
+   from it (with an additional lock).
+ - Fix zeep.helpers.serialize_object() for nested objects (#123)
+ - Remove fallback between soap 1.1 and soap 1.2 namespaces during the parsing
+   of the wsdl. This should not be required.
+
+
+0.12.0 (2016-07-09)
+-------------------
+ - **backwards-incompatible**: Choice elements are now unwrapped if 
+   maxOccurs=1. This results in easier operation definitions when choices are
+   used. 
+ - **backwards-incompatible**: The _soapheader kwarg is renamed to _soapheaders
+   and now requires a nested dictionary with the header name as key or a list
+   of values (value object or lxml.etree.Element object). Please see the 
+   call signature of the function using ``python -mzeep <wsdl>``.
+ - Support the element ref's to xsd:schema elements.
+ - Improve the signature() output of element and type definitions
+ - Accept lxml.etree.Element objects as value for Any elements.
+ - And various other fixes
+ 
+
+0.11.0 (2016-07-03)
+-------------------
+ - **backwards-incompatible**: The kwarg name for Any and Choice elements are
+   renamed to generic ``_value_N`` names.
+ - **backwards-incompatible**: Client.set_address() is replaced with the 
+   Client.create_service() call
+ - Auto-load the http://schemas.xmlsoap.org/soap/encoding/ schema if it is
+   referenced but not imported. Too many XSD's assume that the schema is always
+   available.
+ - Major refactoring of the XSD handling to correctly support nested 
+   xsd:sequence elements. 
+ - Add ``logger.debug()`` calls around Transport.post() to allow capturing the
+   content send/received from the server
+ - Add proper support for default values on attributes and elements.
+
+
+0.10.0 (2016-06-22)
+-------------------
+ - Make global elements / types truly global by refactoring the Schema 
+   parsing. Previously the lookups where non-transitive, but this should only
+   be the case during parsing of the xml schema.
+ - Properly unwrap XML responses in soap.DocumentMessage when a choice is the
+   root element. (#80)
+ - Update exceptions structure, all zeep exceptions are now using 
+   zeep.exceptions.Error() as base class.
+
+
+0.9.1 (2016-06-17)
+------------------
+ - Quote the SOAPAction header value (Derek Harland)
+ - Undo fallback for SOAPAction if it is empty (#83)
+ 
+
+0.9.0 (2016-06-14)
+------------------
+ - Use the appdirs module to retrieve the OS cache path. Note that this results
+   in an other default cache path then previous releases! See 
+   https://github.com/ActiveState/appdirs for more information.
+ - Fix regression when initializing soap objects with invalid kwargs.
+ - Update wsse.UsernameToken to set encoding type on nonce (Antonio Cuni)
+ - Remove assert statement in soap error handling (Eric Waller)
+ - Add '--no-verify' to the command line interface. (#63)
+ - Correctly xsi:type attributes on unbounded elements. (nicholjy) (#68)
+ - Re-implement xsd:list handling
+ - Refactor logic to open files from filesystem.
+ - Refactor the xsd:choice implementation (serializing/deserializing)
+ - Implement parsing of xsd:any elements.
+
+
+0.8.1 (2016-06-08)
+------------------
+ - Use the operation name for the xml element which wraps the parameters in 
+   for soap RPC messages (#60)
+
+
+0.8.0 (2016-06-07)
+------------------
+ - Add ability to override the soap endpoint via `Client.set_address()`
+ - Fix parsing ComplexTypes which have no child elements (#50)
+ - Handle xsi:type attributes on anyType's correctly when deserializing 
+   responses (#17)
+ - Fix xsd:restriction on xsd:simpleType's when the base type wasn't defined
+   yet. (#59)
+ - Add xml declaration to the generate xml strings (#60)
+ - Fix xsd:import statements without schemaLocation (#58)
+
+
+0.7.1 (2016-06-01)
+------------------
+ - Fix regression with handling wsdl:import statements for messages (#47)
+
+
+0.7.0 (2016-05-31)
+------------------
+ - Add support HTTP authentication (mcordes). This adds a new attribute to the
+   Transport client() which passes the http_auth value to requests. (#31)
+ - Fix issue where setting cache=None to Transport class didn't disable 
+   caching.
+ - Refactor handling of wsdl:imports, don't merge definitions but instead 
+   lookup values in child definitions. (#40)
+ - Remove unused namespace declarations from the generated SOAP messages.
+ - Update requirement of six>=1.0.0 to six>=1.9.0 (#39)
+ - Fix handling of xsd:choice, xsd:group and xsd:attribute (#30)
+ - Improve error messages
+ - Fix generating soap messages when sub types are used via xsd extensions (#36)
+ - Improve handling of custom soap headers (#33)
+
+
+0.6.0 (2016-05-21)
+------------------
+ - Add missing `name` attributes to xsd.QName and xsd.NOTATION (#15)
+ - Various fixes related to the Choice element
+ - Support xsd:include
+ - Experimental support for HTTP bindings
+ - Removed `Client.get_port()`, use `Client.bind()`.
+
+
+0.5.0 (2015-05-08)
+------------------
+ - Handle attributes during parsing of the response values>
+ - Don't create empty soap objects when the root element is empty.
+ - Implement support for WSSE usernameToken profile including 
+   passwordText/passwordDigest.
+ - Improve XSD date/time related builtins.
+ - Various minor XSD handling fixes 
+ - Use the correct soap-envelope XML namespace for the Soap 1.2 binding
+ - Use `application/soap+xml` as content-type in the Soap 1.2 binding
+ - **backwards incompatible**: Make cache part of the transport object 
+   instead of the client.  This changes the call signature of the Client() 
+   class.  (Marek Wywiał)
+ - Add the `verify` kwarg to the Transport object to disable ssl certificate
+   verification. (Marek Wywiał)
+
+
+0.4.0 (2016-04-17)
+------------------
+ - Add defusedxml module for XML security issues
+ - Add support for choice elements
+ - Fix documentation example for complex types (Falk Schuetzenmeister)
+
+
+0.3.0 (2016-04-10)
+------------------
+ - Correctly handle recursion in WSDL and XSD files
+ - Add support for the XSD Any element
+ - Allow usage of shorthand prefixes when creating elements and types
+ - And more various improvements
+
+
+0.2.5 (2016-04-05)
+------------------
+ - Temporarily disable the HTTP binding support until it works properly
+ - Fix an issue with parsing SOAP responses with optional elements
+
+
+0.2.4 (2016-04-03)
+------------------
+ - Improve xsd.DateTime, xsd.Date and xsd.Time implementations by using the
+   isodate module.
+ - Implement xsd.Duration 
+
+
+0.2.3 (2016-04-03)
+------------------
+ - Fix xsd.DateTime, xsd.Date and xsd.Time implementations
+ - Handle NIL values correctly for simpletypes
+
+
+0.2.2 (2016-04-03)
+------------------
+ - Fix issue with initializing value objects (ListElements)
+ - Add new `zeep.helpers.serialize_object()` method
+ - Rename type attribute on value objects to `_xsd_type` to remove potential
+   attribute conflicts
+
+
+0.2.1 (2016-04-03)
+------------------
+ - Support minOccurs 0 (optional elements)
+ - Automatically convert python datastructures to zeep objects for requests.
+ - Set default values for new zeep objects to None / [] (Element, ListElement)
+ - Add `Client.get_element()` to create custom objects
+
+
+0.2.0 (2016-04-03)
+------------------
+ - Proper support for XSD element and attribute forms (qualified/unqualified)
+ - Improved XSD handling
+ - Separate bindings for Soap 1.1 and Soap 1.2
+ - And again various other fixes
+
+
+0.1.1 (2016-03-20)
+------------------
+ - Various fixes to make the HttpBinding not throw errors during parsing
+ - More built-in xsd types
+ - Add support for `python -mzeep <wsdl>`
+ - Various other fixes
+
+
+0.1.0 (2016-03-20)
+------------------
+
+Preview / Proof-of-concept release. Probably not suitable for production use :)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9310f1f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,53 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Michael van Tellingen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+Parts of the XSD handling are heavily inspired by soapfish, see:
+https://github.com/FlightDataServices/soapfish
+
+Copyright (c) 2011-2014, soapfish contributors
+All rights reserved.
+For the exact contribution history, see the git revision log.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+3. Neither the name of the copyright holder nor the names of its contributors
+   may be used to endorse or promote products derived from this software without
+   specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3244331
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+.PHONY: install clean test retest coverage docs
+
+install:
+	pip install -e .[docs,test]
+	pip install bumpversion twine wheel
+
+lint:
+	flake8 src/ tests/
+	isort --recursive --check-only --diff src tests
+
+clean:
+	find . -name '*.pyc' -delete
+
+test:
+	py.test -vvv
+
+retest:
+	py.test -vvv --lf
+
+coverage:
+	py.test --cov=zeep --cov-report=term-missing --cov-report=html
+
+docs:
+	$(MAKE) -C docs html
+
+release:
+	pip install twine wheel
+	rm -rf dist/*
+	python setup.py sdist bdist_wheel
+	twine upload -s dist/*
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..0810ebd
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,87 @@
+Metadata-Version: 1.1
+Name: zeep
+Version: 0.23.0
+Summary: A modern/fast Python SOAP client based on lxml / requests
+Home-page: http://docs.python-zeep.org
+Author: Michael van Tellingen
+Author-email: michaelvantellingen at gmail.com
+License: MIT
+Description: ========================
+        Zeep: Python SOAP client 
+        ========================
+        
+        A fast and modern Python SOAP client
+        
+        | Website: http://docs.python-zeep.org/
+        | IRC: #python-zeep on Freenode
+        
+        Highlights:
+         * Modern codebase compatible with Python 2.7, 3.3, 3.4, 3.5 and PyPy
+         * Build on top of lxml and requests
+         * Supports recursive WSDL and XSD documents.
+         * Supports the xsd:choice and xsd:any elements.
+         * Uses the defusedxml module for handling potential XML security issues
+         * Support for WSSE (UsernameToken only for now)
+         * Experimental support for HTTP bindings
+         * Experimental support for WS-Addressing headers
+         * Experimental support for asyncio via aiohttp (Python 3.5+)
+        
+        Features still in development include:
+         * WSSE x.509 support (BinarySecurityToken)
+         * WS Policy support
+        
+        Please see for more information the documentation at
+        http://docs.python-zeep.org/
+        
+        
+        
+        
+        Installation
+        ------------
+        
+        .. code-block:: bash
+        
+            pip install zeep
+        
+        
+        Usage
+        -----
+        .. code-block:: python
+        
+            from zeep import Client
+        
+            client = Client('tests/wsdl_files/example.rst')
+            client.service.ping()
+        
+        
+        To quickly inspect a WSDL file use::
+        
+            python -mzeep <url-to-wsdl>
+        
+        
+        Please see the documentation at http://docs.python-zeep.org for more
+        information.
+        
+        
+        Support
+        =======
+        
+        If you encounter bugs then please `let me know`_ .  A copy of the WSDL file if
+        possible would be most helpful. 
+        
+        I'm also able to offer commercial support.  Please contact me at
+        info at mvantellingen.nl for more information.
+        
+        .. _let me know: https://github.com/mvantellingen/python-zeep/issues
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..81d782f
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,90 @@
+========================
+Zeep: Python SOAP client 
+========================
+
+A fast and modern Python SOAP client
+
+| Website: http://docs.python-zeep.org/
+| IRC: #python-zeep on Freenode
+
+Highlights:
+ * Modern codebase compatible with Python 2.7, 3.3, 3.4, 3.5 and PyPy
+ * Build on top of lxml and requests
+ * Supports recursive WSDL and XSD documents.
+ * Supports the xsd:choice and xsd:any elements.
+ * Uses the defusedxml module for handling potential XML security issues
+ * Support for WSSE (UsernameToken only for now)
+ * Experimental support for HTTP bindings
+ * Experimental support for WS-Addressing headers
+ * Experimental support for asyncio via aiohttp (Python 3.5+)
+
+Features still in development include:
+ * WSSE x.509 support (BinarySecurityToken)
+ * WS Policy support
+
+Please see for more information the documentation at
+http://docs.python-zeep.org/
+
+
+.. start-no-pypi
+
+Status
+------
+
+.. image:: https://readthedocs.org/projects/python-zeep/badge/?version=latest
+    :target: https://readthedocs.org/projects/python-zeep/
+   
+.. image:: https://travis-ci.org/mvantellingen/python-zeep.svg?branch=master
+    :target: https://travis-ci.org/mvantellingen/python-zeep
+
+.. image:: https://ci.appveyor.com/api/projects/status/im609ng9h29vt89r?svg=true
+    :target: https://ci.appveyor.com/project/mvantellingen/python-zeep
+
+.. image:: http://codecov.io/github/mvantellingen/python-zeep/coverage.svg?branch=master 
+    :target: http://codecov.io/github/mvantellingen/python-zeep?branch=master
+
+.. image:: https://img.shields.io/pypi/v/zeep.svg
+    :target: https://pypi.python.org/pypi/zeep/
+
+.. image:: https://requires.io/github/mvantellingen/python-zeep/requirements.svg?branch=master
+     :target: https://requires.io/github/mvantellingen/python-zeep/requirements/?branch=master
+
+.. end-no-pypi
+
+Installation
+------------
+
+.. code-block:: bash
+
+    pip install zeep
+
+
+Usage
+-----
+.. code-block:: python
+
+    from zeep import Client
+
+    client = Client('tests/wsdl_files/example.rst')
+    client.service.ping()
+
+
+To quickly inspect a WSDL file use::
+
+    python -mzeep <url-to-wsdl>
+
+
+Please see the documentation at http://docs.python-zeep.org for more
+information.
+
+
+Support
+=======
+
+If you encounter bugs then please `let me know`_ .  A copy of the WSDL file if
+possible would be most helpful. 
+
+I'm also able to offer commercial support.  Please contact me at
+info at mvantellingen.nl for more information.
+
+.. _let me know: https://github.com/mvantellingen/python-zeep/issues
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..49c4227
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,216 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  applehelp  to make an Apple Help Book"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  xml        to make Docutils-native XML files"
+	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+	@echo "  coverage   to run coverage check of the documentation (if enabled)"
+
+.PHONY: clean
+clean:
+	rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Zeep.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Zeep.qhc"
+
+.PHONY: applehelp
+applehelp:
+	$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+	@echo
+	@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+	@echo "N.B. You won't be able to view it unless you put it in" \
+	      "~/Library/Documentation/Help or install it in your application" \
+	      "bundle."
+
+.PHONY: devhelp
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/Zeep"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Zeep"
+	@echo "# devhelp"
+
+.PHONY: epub
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: latex
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through platex and dvipdfmx..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+	@echo "Testing of coverage in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+	@echo
+	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+	@echo
+	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/_templates/sidebar-intro.html b/docs/_templates/sidebar-intro.html
new file mode 100644
index 0000000..af3fb9e
--- /dev/null
+++ b/docs/_templates/sidebar-intro.html
@@ -0,0 +1,18 @@
+
+<a href="{{ pathto(master_doc) }}">
+    <img class="logo" src="{{ pathto('_static/zeep-logo.png', 1) }}">
+</a>
+
+<p>Zeep is a modern SOAP client for Python</p>
+<p>
+  <iframe src="http://ghbtns.com/github-btn.html?user=mvantellingen&repo=python-zeep&type=watch&count=true&size=large"
+    allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
+</p>
+
+<h3>Links</h3>
+<ul>
+  <li><a href="http://github.com/mvantellingen/python-zeep">Zeep @ GitHub</a></li>
+  <li><a href="http://pypi.python.org/pypi/zeep">Zeep @ PyPI</a></li>
+  <li><a href="http://github.com/mvantellingen/python-zeep/issues">Issue Tracker</a></li>
+</ul>
+
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..b233225
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*-
+#
+# Zeep documentation build configuration file, created by
+# sphinx-quickstart on Fri Mar  4 16:51:06 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+import pkg_resources
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Zeep'
+copyright = u'2016, <a href="https://www.mvantellingen.nl/">Michael van Tellingen</a>'
+author = u'Michael van Tellingen'
+
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.23.0'
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+autodoc_default_flags = [':members:']
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'alabaster'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {
+    'github_user': 'mvantellingen',
+    'github_banner': True,
+    'github_repo': 'python-zeep',
+    'travis_button': True,
+    'codecov_button': True,
+    'analytics_id':  'UA-75907833-1',
+}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+html_sidebars = {
+    '*': [
+        'sidebar-intro.html', 'globaltoc.html', 'sourcelink.html',
+        'searchbox.html'
+    ]
+}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Zeepdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'Zeep.tex', u'Zeep Documentation',
+     u'Michael van Tellingen', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'zeep', u'Zeep Documentation',
+     [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'Zeep', u'Zeep Documentation',
+     author, 'Zeep', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..2aeb016
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,131 @@
+========================
+Zeep: Python SOAP client 
+========================
+
+A fast and modern Python SOAP client
+
+Highlights:
+ * Modern codebase compatible with Python 2.7, 3.3, 3.4, 3.5 and PyPy
+ * Build on top of lxml and requests
+ * Supports recursive WSDL and XSD documents.
+ * Supports the xsd:choice and xsd:any elements.
+ * Uses the defusedxml module for handling potential XML security issues
+ * Support for WSSE (UsernameToken only for now)
+ * Experimental support for HTTP bindings
+ * Experimental support for WS-Addressing headers
+ * Experimental support for asyncio via aiohttp (Python 3.5+)
+
+Features still in development include:
+ * WSSE x.509 support (BinarySecurityToken)
+ * WS Policy support
+
+
+A simple example:
+
+.. code-block:: python
+
+    from zeep import Client
+
+    client = Client('http://www.webservicex.net/ConvertSpeed.asmx?WSDL')
+    result = client.service.ConvertSpeed(
+        100, 'kilometersPerhour', 'milesPerhour')
+
+    assert result == 62.137
+
+
+Quick Introduction
+==================
+
+Zeep inspects the wsdl document and generates the corresponding bindings.  This
+provides an easy to use programmatic interface to a soap server.
+
+The emphasis is on Soap 1.1 and Soap 1.2, however Zeep also offers experimental
+support for HTTP Get and Post bindings.
+
+Parsing the XML documents is done by using the lxml library. This is the most
+performant and compliant Python XML library currently available. This results
+in major speed benefits when retrieving large soap responses.
+
+The SOAP specifications are unfortunately really vague and leave a lot of
+things open for interpretation.  Due to this there are a lot of WSDL documents
+available which are invalid or SOAP servers which contain bugs. Zeep tries to
+be as compatible as possible but there might be cases where you run into 
+problems. Don't hesitate to submit an issue in this case (please see 
+:ref:`reporting_bugs`).
+
+
+Getting started
+===============
+
+You can install the latest version of zeep using pip::
+
+    pip install zeep
+
+The first thing you generally want to do is inspect the wsdl file you need to
+implement. This can be done with::
+
+    python -mzeep <wsdl>
+
+
+See ``python -mzeep --help`` for more information about this command.
+
+
+.. note:: Since this module hasn't reached 1.0.0 yet their might be minor
+          releases which introduce backwards compatible changes. While I try 
+          to keep this to a minimum it can still happen. So as always pin the 
+          version of zeep you used (e.g. ``zeep==0.14.0``').
+
+
+
+A simple use-case
+-----------------
+
+To give you an idea how zeep works a basic example.
+
+.. code-block:: python
+
+    import zeep
+
+    wsdl = 'http://www.soapclient.com/xml/soapresponder.wsdl'
+    client = zeep.Client(wsdl=wsdl)
+    print(client.service.Method1('Zeep', 'is cool'))
+
+The WSDL used above only defines one simple function (``Method1``) which is 
+made available by zeep via ``client.service.Method1``. It takes two arguments
+and returns a string. To get an overview of the services available on the 
+endpoint you can run the following command in your terminal.
+
+.. code-block:: bash
+
+    python -mzeep http://www.soapclient.com/xml/soapresponder.wsdl
+
+
+More information
+================
+
+.. toctree::
+   :maxdepth: 2
+   :name: mastertoc
+
+   in_depth
+   datastructures
+   transport
+   wsa
+   wsse
+   plugins
+   helpers
+   reporting_bugs
+   changes
+
+
+Support
+=======
+
+If you encounter bugs then please `let me know`_ . Please see :doc:`reporting_bugs`
+for information how to best report them.
+
+I'm also able to offer commercial support.  Please contact me at
+info at mvantellingen.nl for more information.
+
+
+.. _let me know: https://github.com/mvantellingen/python-zeep/issues
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..bb121e7
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,263 @@
+ at ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html       to make standalone HTML files
+	echo.  dirhtml    to make HTML files named index.html in directories
+	echo.  singlehtml to make a single large HTML file
+	echo.  pickle     to make pickle files
+	echo.  json       to make JSON files
+	echo.  htmlhelp   to make HTML files and a HTML help project
+	echo.  qthelp     to make HTML files and a qthelp project
+	echo.  devhelp    to make HTML files and a Devhelp project
+	echo.  epub       to make an epub
+	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  text       to make text files
+	echo.  man        to make manual pages
+	echo.  texinfo    to make Texinfo files
+	echo.  gettext    to make PO message catalogs
+	echo.  changes    to make an overview over all changed/added/deprecated items
+	echo.  xml        to make Docutils-native XML files
+	echo.  pseudoxml  to make pseudoxml-XML files for display purposes
+	echo.  linkcheck  to check all external links for integrity
+	echo.  doctest    to run all doctests embedded in the documentation if enabled
+	echo.  coverage   to run coverage check of the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 1>NUL 2>NUL
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "singlehtml" (
+	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Zeep.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Zeep.ghc
+	goto end
+)
+
+if "%1" == "devhelp" (
+	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished.
+	goto end
+)
+
+if "%1" == "epub" (
+	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub file is in %BUILDDIR%/epub.
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "latexpdf" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	cd %BUILDDIR%/latex
+	make all-pdf
+	cd %~dp0
+	echo.
+	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "latexpdfja" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	cd %BUILDDIR%/latex
+	make all-pdf-ja
+	cd %~dp0
+	echo.
+	echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "text" (
+	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The text files are in %BUILDDIR%/text.
+	goto end
+)
+
+if "%1" == "man" (
+	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The manual pages are in %BUILDDIR%/man.
+	goto end
+)
+
+if "%1" == "texinfo" (
+	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+	goto end
+)
+
+if "%1" == "gettext" (
+	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+if "%1" == "coverage" (
+	%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+	goto end
+)
+
+if "%1" == "xml" (
+	%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The XML files are in %BUILDDIR%/xml.
+	goto end
+)
+
+if "%1" == "pseudoxml" (
+	%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+	goto end
+)
+
+:end
diff --git a/examples/code39.py b/examples/code39.py
new file mode 100644
index 0000000..eda664f
--- /dev/null
+++ b/examples/code39.py
@@ -0,0 +1,8 @@
+from __future__ import print_function
+import zeep
+
+
+client = zeep.Client(
+    wsdl='http://www.webservicex.net/barcode.asmx?WSDL')
+response = client.service.Code39('1234', 20, ShowCodeString=True, Title='ZEEP')
+print(repr(response))
diff --git a/examples/echo_services.py b/examples/echo_services.py
new file mode 100644
index 0000000..5270f95
--- /dev/null
+++ b/examples/echo_services.py
@@ -0,0 +1,5 @@
+from zeep.client import Client
+
+# RPC style soap service
+client = Client('http://www.soapclient.com/xml/soapresponder.wsdl')
+print(client.service.Method1('zeep', 'soap'))
diff --git a/examples/eu_vat_service.py b/examples/eu_vat_service.py
new file mode 100644
index 0000000..e5ba15e
--- /dev/null
+++ b/examples/eu_vat_service.py
@@ -0,0 +1,7 @@
+from __future__ import print_function
+import zeep
+
+
+client = zeep.Client(
+    wsdl='http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl')
+print(client.service.checkVat('NL', '170944128B01'))
diff --git a/examples/km_to_miles.py b/examples/km_to_miles.py
new file mode 100644
index 0000000..d8821df
--- /dev/null
+++ b/examples/km_to_miles.py
@@ -0,0 +1,16 @@
+from __future__ import print_function
+import zeep
+
+
+client = zeep.Client(
+    wsdl='http://www.webservicex.net/ConvertSpeed.asmx?WSDL')
+
+client.wsdl.dump()
+
+print (client.service.ConvertSpeed(100, 'kilometersPerhour', 'milesPerhour'))
+
+http_get = client.bind('ConvertSpeeds', 'ConvertSpeedsHttpGet')
+http_post = client.bind('ConvertSpeeds', 'ConvertSpeedsHttpPost')
+
+print(http_get.ConvertSpeed(100, 'kilometersPerhour', 'milesPerhour'))
+print(http_post.ConvertSpeed(100, 'kilometersPerhour', 'milesPerhour'))
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3cbf7f8
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,11 @@
+[wheel]
+universal = 1
+
+[flake8]
+max-line-length = 99
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..8ba07bf
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,72 @@
+import re
+from setuptools import find_packages, setup
+
+install_requires = [
+    'appdirs>=1.4.0',
+    'cached-property>=1.0.0',
+    'defusedxml>=0.4.1',
+    'isodate>=0.5.4',
+    'lxml>=3.0.0',
+    'requests>=2.7.0',
+    'six>=1.9.0',
+    'pytz',
+]
+
+docs_require = [
+    'sphinx>=1.4.0',
+]
+
+tests_require = [
+    'freezegun==0.3.7',
+    'mock==2.0.0',
+    'pretend==1.0.8',
+    'pytest-cov==2.3.1',
+    'pytest==3.0.2',
+    'requests_mock>=0.7.0',
+
+    # Linting
+    'isort==4.2.5',
+    'flake8==3.0.3',
+    'flake8-blind-except==0.1.1',
+    'flake8-debugger==1.4.0',
+]
+
+with open('README.rst') as fh:
+    long_description = re.sub(
+        '^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
+
+setup(
+    name='zeep',
+    version='0.23.0',
+    description='A modern/fast Python SOAP client based on lxml / requests',
+    long_description=long_description,
+    author="Michael van Tellingen",
+    author_email="michaelvantellingen at gmail.com",
+    url='http://docs.python-zeep.org',
+
+    install_requires=install_requires,
+    tests_require=tests_require,
+    extras_require={
+        'docs': docs_require,
+        'test': tests_require,
+    },
+    entry_points={},
+    package_dir={'': 'src'},
+    packages=find_packages('src'),
+    include_package_data=True,
+
+    license='MIT',
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'License :: OSI Approved :: MIT License',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: Implementation :: CPython',
+        'Programming Language :: Python :: Implementation :: PyPy',
+    ],
+    zip_safe=False,
+)
diff --git a/src/zeep/__init__.py b/src/zeep/__init__.py
new file mode 100644
index 0000000..606d054
--- /dev/null
+++ b/src/zeep/__init__.py
@@ -0,0 +1,5 @@
+from zeep.client import Client  # noqa
+from zeep.transports import Transport  # noqa
+from zeep.plugins import Plugin  # noqa
+
+__version__ = '0.23.0'
diff --git a/src/zeep/__main__.py b/src/zeep/__main__.py
new file mode 100644
index 0000000..ae2c3af
--- /dev/null
+++ b/src/zeep/__main__.py
@@ -0,0 +1,85 @@
+from __future__ import absolute_import, print_function
+
+import argparse
+import logging
+import logging.config
+import time
+
+from six.moves.urllib.parse import urlparse
+from zeep.cache import InMemoryCache, SqliteCache
+from zeep.client import Client
+from zeep.transports import Transport
+
+logger = logging.getLogger('zeep')
+
+
+def parse_arguments(args=None):
+    parser = argparse.ArgumentParser(description='Zeep: The SOAP client')
+    parser.add_argument(
+        'wsdl_file', type=str, help='Path or URL to the WSDL file',
+        default=None)
+    parser.add_argument(
+        '--cache', action='store_true', help='Enable cache')
+    parser.add_argument(
+        '--no-verify', action='store_true', help='Disable SSL verification')
+    parser.add_argument(
+        '--verbose', action='store_true', help='Enable verbose output')
+    parser.add_argument(
+        '--profile', help="Enable profiling and save output to given file")
+    return parser.parse_args(args)
+
+
+def main(args):
+    if args.verbose:
+        logging.config.dictConfig({
+            'version': 1,
+            'formatters': {
+                'verbose': {
+                    'format': '%(name)20s: %(message)s'
+                }
+            },
+            'handlers': {
+                'console': {
+                    'level': 'DEBUG',
+                    'class': 'logging.StreamHandler',
+                    'formatter': 'verbose',
+                },
+            },
+            'loggers': {
+                'zeep': {
+                    'level': 'DEBUG',
+                    'propagate': True,
+                    'handlers': ['console'],
+                },
+            }
+        })
+
+    if args.profile:
+        import cProfile
+        profile = cProfile.Profile()
+        profile.enable()
+
+    cache = SqliteCache() if args.cache else InMemoryCache()
+    transport_kwargs = {'cache': cache}
+
+    if args.no_verify:
+        transport_kwargs['verify'] = False
+
+    result = urlparse(args.wsdl_file)
+    if result.username or result.password:
+        transport_kwargs['http_auth'] = (result.username, result.password)
+
+    transport = Transport(**transport_kwargs)
+    st = time.time()
+    client = Client(args.wsdl_file, transport=transport)
+    logger.debug("Loading WSDL took %sms", (time.time() - st) * 1000)
+
+    if args.profile:
+        profile.disable()
+        profile.dump_stats(args.profile)
+    client.wsdl.dump()
+
+
+if __name__ == '__main__':
+    args = parse_arguments()
+    main(args)
diff --git a/src/zeep/asyncio/__init__.py b/src/zeep/asyncio/__init__.py
new file mode 100644
index 0000000..3011239
--- /dev/null
+++ b/src/zeep/asyncio/__init__.py
@@ -0,0 +1,2 @@
+from .transport import *  # noqa
+from .bindings import *  # noqa
diff --git a/src/zeep/asyncio/bindings.py b/src/zeep/asyncio/bindings.py
new file mode 100644
index 0000000..63c8f21
--- /dev/null
+++ b/src/zeep/asyncio/bindings.py
@@ -0,0 +1,26 @@
+from zeep.wsdl import bindings
+
+__all__ = ['AsyncSoap11Binding', 'AsyncSoap12Binding']
+
+
+class AsyncSoapBinding(object):
+
+    async def send(self, client, options, operation, args, kwargs):
+        envelope, http_headers = self._create(
+            operation, args, kwargs,
+            client=client,
+            options=options)
+
+        response = await client.transport.post_xml(
+            options['address'], envelope, http_headers)
+
+        operation_obj = self.get(operation)
+        return self.process_reply(client, operation_obj, response)
+
+
+class AsyncSoap11Binding(AsyncSoapBinding, bindings.Soap11Binding):
+    pass
+
+
+class AsyncSoap12Binding(AsyncSoapBinding, bindings.Soap12Binding):
+    pass
diff --git a/src/zeep/asyncio/transport.py b/src/zeep/asyncio/transport.py
new file mode 100644
index 0000000..c487811
--- /dev/null
+++ b/src/zeep/asyncio/transport.py
@@ -0,0 +1,73 @@
+"""
+Adds asyncio support to Zeep. Contains Python 3.5+ only syntax!
+
+"""
+import asyncio
+
+import aiohttp
+
+from zeep.transports import Transport
+from zeep.wsdl.utils import etree_to_string
+
+__all__ = ['AsyncTransport']
+
+
+class AsyncTransport(Transport):
+    """Asynchronous Transport class using aiohttp."""
+    supports_async = True
+
+    def __init__(self, loop, *args, **kwargs):
+        self.loop = loop if loop else asyncio.get_event_loop()
+        super().__init__(*args, **kwargs)
+
+    def create_session(self):
+        connector = aiohttp.TCPConnector(verify_ssl=self.http_verify)
+
+        return aiohttp.ClientSession(
+            connector=connector,
+            loop=self.loop,
+            headers=self.http_headers,
+            auth=self.http_auth)
+
+    def _load_remote_data(self, url):
+        result = None
+        async def _load_remote_data_async():
+            nonlocal result
+            with aiohttp.Timeout(self.load_timeout):
+                response = await self.session.get(url)
+                result = await response.read()
+
+        # Block until we have the data
+        self.loop.run_until_complete(_load_remote_data_async())
+        return result
+
+    async def post(self, address, message, headers):
+        self.logger.debug("HTTP Post to %s:\n%s", address, message)
+        with aiohttp.Timeout(self.operation_timeout):
+            response = await self.session.post(
+                address, data=message, headers=headers)
+            self.logger.debug(
+                "HTTP Response from %s (status: %d):\n%s",
+                address, response.status, await response.read())
+            return response
+
+    async def post_xml(self, address, envelope, headers):
+        message = etree_to_string(envelope)
+        response = await self.post(address, message, headers)
+
+        from pretend import stub
+        return stub(
+            content=await response.read(),
+            status_code=response.status,
+            headers=response.headers)
+
+    async def get(self, address, params, headers):
+        with aiohttp.Timeout(self.operation_timeout):
+            response = await self.session.get(
+                address, params=params, headers=headers)
+
+            from pretend import stub
+            return await stub(
+                content=await response.read(),
+                status_code=response.status,
+                headers=response.headers)
diff --git a/src/zeep/cache.py b/src/zeep/cache.py
new file mode 100644
index 0000000..27e8d2c
--- /dev/null
+++ b/src/zeep/cache.py
@@ -0,0 +1,158 @@
+import base64
+import datetime
+import errno
+import logging
+import os
+import threading
+from contextlib import contextmanager
+
+import appdirs
+import pytz
+import six
+
+# The sqlite3 is not available on Google App Engine so we handle the
+# ImportError here and set the sqlite3 var to None.
+# See https://github.com/mvantellingen/python-zeep/issues/243
+try:
+    import sqlite3
+except ImportError:
+    sqlite3 = None
+
+logger = logging.getLogger(__name__)
+
+
+class Base(object):
+
+    def add(self, url, content):
+        raise NotImplemented()
+
+    def get(self, url):
+        raise NotImplemented()
+
+
+class InMemoryCache(Base):
+    """Simple in-memory caching using dict lookup with support for timeouts"""
+    _cache = {}  # global cache, thread-safe by default
+
+    def __init__(self, timeout=3600):
+        self._timeout = timeout
+
+    def add(self, url, content):
+        logger.debug("Caching contents of %s", url)
+        self._cache[url] = (datetime.datetime.utcnow(), content)
+
+    def get(self, url):
+        try:
+            created, content = self._cache[url]
+        except KeyError:
+            pass
+        else:
+            if not _is_expired(created, self._timeout):
+                logger.debug("Cache HIT for %s", url)
+                return content
+        logger.debug("Cache MISS for %s", url)
+        return None
+
+
+class SqliteCache(Base):
+    """Cache contents via an sqlite database on the filesystem"""
+    _version = '1'
+
+    def __init__(self, path=None, timeout=3600):
+
+        if sqlite3 is None:
+            raise RuntimeError("sqlite3 module is required for the SqliteCache")
+
+        # No way we can support this when we want to achieve thread safety
+        if path == ':memory:':
+            raise ValueError(
+                "The SqliteCache doesn't support :memory: since it is not " +
+                "thread-safe. Please use zeep.cache.InMemoryCache()")
+
+        self._lock = threading.RLock()
+        self._timeout = timeout
+        self._db_path = path if path else _get_default_cache_path()
+
+        # Initialize db
+        with self.db_connection() as conn:
+            cursor = conn.cursor()
+            cursor.execute(
+                """
+                    CREATE TABLE IF NOT EXISTS request
+                    (created timestamp, url text, content text)
+                """)
+            conn.commit()
+
+    @contextmanager
+    def db_connection(self):
+        with self._lock:
+            connection = sqlite3.connect(
+                self._db_path, detect_types=sqlite3.PARSE_DECLTYPES)
+            yield connection
+            connection.close()
+
+    def add(self, url, content):
+        logger.debug("Caching contents of %s", url)
+        data = self._encode_data(content)
+
+        with self.db_connection() as conn:
+            cursor = conn.cursor()
+            cursor.execute("DELETE FROM request WHERE url = ?", (url,))
+            cursor.execute(
+                "INSERT INTO request (created, url, content) VALUES (?, ?, ?)",
+                (datetime.datetime.utcnow(), url, data))
+            conn.commit()
+
+    def get(self, url):
+        with self.db_connection() as conn:
+            cursor = conn.cursor()
+            cursor.execute(
+                "SELECT created, content FROM request WHERE url=?", (url, ))
+            rows = cursor.fetchall()
+
+        if rows:
+            created, data = rows[0]
+            if not _is_expired(created, self._timeout):
+                logger.debug("Cache HIT for %s", url)
+                return self._decode_data(data)
+        logger.debug("Cache MISS for %s", url)
+
+    def _encode_data(self, data):
+        data = base64.b64encode(data)
+        if six.PY2:
+            return buffer(self._version_string + data)  # noqa
+        return self._version_string + data
+
+    def _decode_data(self, data):
+        if six.PY2:
+            data = str(data)
+        if data.startswith(self._version_string):
+            return base64.b64decode(data[len(self._version_string):])
+
+    @property
+    def _version_string(self):
+        prefix = u'$ZEEP:%s$' % self._version
+        return bytes(prefix.encode('ascii'))
+
+
+def _is_expired(value, timeout):
+    """Return boolean if the value is expired"""
+    if timeout is None:
+        return False
+
+    now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
+    max_age = value.replace(tzinfo=pytz.utc)
+    max_age += datetime.timedelta(seconds=timeout)
+    return now > max_age
+
+
+def _get_default_cache_path():
+    path = appdirs.user_cache_dir('zeep', False)
+    try:
+        os.makedirs(path)
+    except OSError as exc:
+        if exc.errno == errno.EEXIST and os.path.isdir(path):
+            pass
+        else:
+            raise
+    return os.path.join(path, 'cache.db')
diff --git a/src/zeep/client.py b/src/zeep/client.py
new file mode 100644
index 0000000..62bfcec
--- /dev/null
+++ b/src/zeep/client.py
@@ -0,0 +1,188 @@
+import copy
+import logging
+from contextlib import contextmanager
+
+from zeep.transports import Transport
+from zeep.wsdl import Document
+
+NSMAP = {
+    'xsd': 'http://www.w3.org/2001/XMLSchema',
+    'soap': 'http://schemas.xmlsoap.org/wsdl/soap/',
+    'soap-env': 'http://schemas.xmlsoap.org/soap/envelope/',
+}
+
+
+logger = logging.getLogger(__name__)
+
+
+class OperationProxy(object):
+    def __init__(self, service_proxy, operation_name):
+        self._proxy = service_proxy
+        self._op_name = operation_name
+
+    def __call__(self, *args, **kwargs):
+        if self._proxy._client._default_soapheaders:
+            op_soapheaders = kwargs.get('_soapheaders')
+            if op_soapheaders:
+                soapheaders = copy.deepcopy(self._proxy._client._default_soapheaders)
+                if type(op_soapheaders) != type(soapheaders):
+                    raise ValueError("Incompatible soapheaders definition")
+
+                if isinstance(soapheaders, list):
+                    soapheaders.extend(op_soapheaders)
+                else:
+                    soapheaders.update(op_soapheaders)
+            else:
+                soapheaders = self._proxy._client._default_soapheaders
+            kwargs['_soapheaders'] = soapheaders
+
+        return self._proxy._binding.send(
+            self._proxy._client, self._proxy._binding_options,
+            self._op_name, args, kwargs)
+
+
+class ServiceProxy(object):
+    def __init__(self, client, binding, **binding_options):
+        self._client = client
+        self._binding_options = binding_options
+        self._binding = binding
+
+    def __getattr__(self, key):
+        return self[key]
+
+    def __getitem__(self, key):
+        try:
+            self._binding.get(key)
+        except ValueError:
+            raise AttributeError('Service has no operation %r' % key)
+        return OperationProxy(self, key)
+
+
+class Factory(object):
+    def __init__(self, types, kind, namespace):
+        self._method = getattr(types, 'get_%s' % kind)
+
+        if namespace in types.namespaces:
+            self._ns = namespace
+        else:
+            self._ns = types.get_ns_prefix(namespace)
+
+    def __getattr__(self, key):
+        return self[key]
+
+    def __getitem__(self, key):
+        return self._method('{%s}%s' % (self._ns, key))
+
+
+class Client(object):
+
+    def __init__(self, wsdl, wsse=None, transport=None,
+                 service_name=None, port_name=None, plugins=None):
+        if not wsdl:
+            raise ValueError("No URL given for the wsdl")
+
+        self.transport = transport or Transport()
+        self.wsdl = Document(wsdl, self.transport)
+        self.wsse = wsse
+        self.plugins = plugins if plugins is not None else []
+
+        self._default_service = None
+        self._default_service_name = service_name
+        self._default_port_name = port_name
+        self._default_soapheaders = None
+
+    @property
+    def service(self):
+        """The default ServiceProxy instance"""
+        if self._default_service:
+            return self._default_service
+
+        self._default_service = self.bind(
+            service_name=self._default_service_name,
+            port_name=self._default_port_name)
+        if not self._default_service:
+            raise ValueError(
+                "There is no default service defined. This is usually due to "
+                "missing wsdl:service definitions in the WSDL")
+        return self._default_service
+
+    @contextmanager
+    def options(self, timeout):
+        """Context manager to temporarily overrule various options.
+
+        Example::
+
+            client = zeep.Client('foo.wsdl')
+            with client.options(timeout=10):
+                client.service.fast_call()
+
+        :param timeout: Set the timeout for POST/GET operations (not used for
+                        loading external WSDL or XSD documents)
+
+        """
+        with self.transport._options(timeout=timeout):
+            yield
+
+    def bind(self, service_name=None, port_name=None):
+        """Create a new ServiceProxy for the given service_name and port_name.
+
+        The default ServiceProxy instance (`self.service`) always referes to
+        the first service/port in the wsdl Document.  Use this when a specific
+        port is required.
+
+        """
+        if not self.wsdl.services:
+            return
+
+        if service_name:
+            service = self.wsdl.services.get(service_name)
+            if not service:
+                raise ValueError("Service not found")
+        else:
+            service = next(iter(self.wsdl.services.values()), None)
+
+        if port_name:
+            port = service.ports.get(port_name)
+            if not port:
+                raise ValueError("Port not found")
+        else:
+            port = list(service.ports.values())[0]
+        return ServiceProxy(self, port.binding, **port.binding_options)
+
+    def create_service(self, binding_name, address):
+        """Create a new ServiceProxy for the given binding name and address.
+
+        :param binding_name: The QName of the binding
+        :param address: The address of the endpoint
+
+        """
+        try:
+            binding = self.wsdl.bindings[binding_name]
+        except KeyError:
+            raise ValueError(
+                "No binding found with the given QName. Available bindings "
+                "are: %s" % (', '.join(self.wsdl.bindings.keys())))
+        return ServiceProxy(self, binding, address=address)
+
+    def type_factory(self, namespace):
+        return Factory(self.wsdl.types, 'type', namespace)
+
+    def get_type(self, name):
+        return self.wsdl.types.get_type(name)
+
+    def get_element(self, name):
+        return self.wsdl.types.get_element(name)
+
+    def set_ns_prefix(self, prefix, namespace):
+        self.wsdl.types.set_ns_prefix(prefix, namespace)
+
+    def set_default_soapheaders(self, headers):
+        """Set the default soap headers which will be automatically used on
+        all calls.
+
+        Note that if you pass custom soapheaders using a list then you will
+        also need to use that during the operations. Since mixing these use
+        cases isn't supported (yet).
+
+        """
+        self._default_soapheaders = headers
diff --git a/src/zeep/exceptions.py b/src/zeep/exceptions.py
new file mode 100644
index 0000000..bdd2334
--- /dev/null
+++ b/src/zeep/exceptions.py
@@ -0,0 +1,49 @@
+class Error(Exception):
+    def __init__(self, message):
+        super(Exception, self).__init__(message)
+        self.message = message
+
+    def __repr__(self):
+        return '%s(%r)' % (self.__class__.__name__, self.message)
+
+
+class XMLSyntaxError(Error):
+    pass
+
+
+class XMLParseError(Error):
+    pass
+
+
+class UnexpectedElementError(Error):
+    pass
+
+
+class WsdlSyntaxError(Error):
+    pass
+
+
+class TransportError(Error):
+    pass
+
+
+class LookupError(Error):
+    pass
+
+
+class NamespaceError(Error):
+    pass
+
+
+class Fault(Error):
+    def __init__(self, message, code=None, actor=None, detail=None, subcodes=None):
+        super(Fault, self).__init__(message)
+        self.message = message
+        self.code = code
+        self.actor = actor
+        self.detail = detail
+        self.subcodes = subcodes
+
+
+class ZeepWarning(RuntimeWarning):
+    pass
diff --git a/src/zeep/helpers.py b/src/zeep/helpers.py
new file mode 100644
index 0000000..27df819
--- /dev/null
+++ b/src/zeep/helpers.py
@@ -0,0 +1,20 @@
+from collections import OrderedDict
+
+from zeep.xsd.valueobjects import CompoundValue
+
+
+def serialize_object(obj):
+    """Serialize zeep objects to native python data structures"""
+    if obj is None:
+        return obj
+
+    if isinstance(obj, list):
+        return [serialize_object(sub) for sub in obj]
+
+    result = OrderedDict()
+    for key in obj:
+        value = obj[key]
+        if isinstance(value, (list, CompoundValue)):
+            value = serialize_object(value)
+        result[key] = value
+    return result
diff --git a/src/zeep/parser.py b/src/zeep/parser.py
new file mode 100644
index 0000000..4589def
--- /dev/null
+++ b/src/zeep/parser.py
@@ -0,0 +1,40 @@
+import os
+
+from defusedxml.lxml import fromstring
+from lxml import etree
+
+from six.moves.urllib.parse import urljoin, urlparse
+from zeep.exceptions import XMLSyntaxError
+
+
+def parse_xml(content, base_url=None, recover=False):
+    parser = etree.XMLParser(remove_comments=True, recover=recover)
+    try:
+        return fromstring(content, parser=parser, base_url=base_url)
+    except etree.XMLSyntaxError as exc:
+        raise XMLSyntaxError("Invalid XML content received (%s)" % exc)
+
+
+def load_external(url, transport, base_url=None):
+    if base_url:
+        url = absolute_location(url, base_url)
+
+    response = transport.load(url)
+    return parse_xml(response, base_url)
+
+
+def absolute_location(location, base):
+    if location == base or location.startswith('intschema'):
+        return location
+
+    if urlparse(location).scheme in ('http', 'https'):
+        return location
+
+    if base and urlparse(base).scheme in ('http', 'https'):
+        return urljoin(base, location)
+    else:
+        if os.path.isabs(location):
+            return location
+        if base:
+            return os.path.join(os.path.dirname(base), location)
+    return location
diff --git a/src/zeep/plugins.py b/src/zeep/plugins.py
new file mode 100644
index 0000000..5ddd953
--- /dev/null
+++ b/src/zeep/plugins.py
@@ -0,0 +1,63 @@
+from collections import deque
+
+
+class Plugin(object):
+    """Base plugin"""
+
+    def ingress(self, envelope, http_headers, operation):
+        return envelope, http_headers
+
+    def egress(self, envelope, http_headers, operation, binding_options):
+        return envelope, http_headers
+
+
+def apply_egress(client, envelope, http_headers, operation, binding_options):
+    for plugin in client.plugins:
+        result = plugin.egress(
+            envelope, http_headers, operation, binding_options)
+        if result is not None:
+            envelope, http_headers = result
+
+    return envelope, http_headers
+
+
+def apply_ingress(client, envelope, http_headers, operation):
+    for plugin in client.plugins:
+        result = plugin.ingress(envelope, http_headers, operation)
+        if result is not None:
+            envelope, http_headers = result
+
+    return envelope, http_headers
+
+
+class HistoryPlugin(object):
+    def __init__(self, maxlen=1):
+        self._buffer = deque([], maxlen)
+
+    @property
+    def last_sent(self):
+        last_tx = self._buffer[-1]
+        if last_tx:
+            return last_tx['sent']
+
+    @property
+    def last_received(self):
+        last_tx = self._buffer[-1]
+        if last_tx:
+            return last_tx['received']
+
+    def ingress(self, envelope, http_headers, operation):
+        last_tx = self._buffer[-1]
+        last_tx['received'] = {
+            'envelope': envelope,
+            'http_headers': http_headers,
+        }
+
+    def egress(self, envelope, http_headers, operation, binding_options):
+        self._buffer.append({
+            'received': None,
+            'sent': {
+                'envelope': envelope,
+                'http_headers': http_headers,
+            },
+        })
diff --git a/src/zeep/transports.py b/src/zeep/transports.py
new file mode 100644
index 0000000..a4d71ed
--- /dev/null
+++ b/src/zeep/transports.py
@@ -0,0 +1,154 @@
+import logging
+import os
+from contextlib import contextmanager
+
+import requests
+
+from six.moves.urllib.parse import urlparse
+from zeep.cache import SqliteCache
+from zeep.utils import NotSet, get_version
+from zeep.wsdl.utils import etree_to_string
+
+
+class Transport(object):
+    supports_async = False
+
+    def __init__(self, cache=NotSet, timeout=300, operation_timeout=None,
+                 verify=True, http_auth=None):
+        """The transport object handles all communication to the SOAP server.
+
+        :param cache: The cache object to be used to cache GET requests
+        :param timeout: The timeout for loading wsdl and xsd documents.
+        :param operation_timeout: The timeout for operations (POST/GET). By
+                                  default this is None (no timeout).
+        :param verify: Boolean to indicate if the SSL certificate needs to be
+                       verified.
+        :param http_auth: HTTP authentication, passed to requests.
+
+        """
+        self.cache = SqliteCache() if cache is NotSet else cache
+        self.load_timeout = timeout
+        self.operation_timeout = operation_timeout
+        self.logger = logging.getLogger(__name__)
+
+        self.http_verify = verify
+        self.http_auth = http_auth
+        self.http_headers = {
+            'User-Agent': 'Zeep/%s (www.python-zeep.org)' % (get_version())
+        }
+        self.session = self.create_session()
+
+    def create_session(self):
+        session = requests.Session()
+        session.verify = self.http_verify
+        session.auth = self.http_auth
+        session.headers = self.http_headers
+        return session
+
+    def get(self, address, params, headers):
+        """Proxy to requests.get()
+
+        :param address: The URL for the request
+        :param params: The query parameters
+        :param headers: a dictionary with the HTTP headers.
+
+        """
+        response = self.session.get(
+            address,
+            params=params,
+            headers=headers,
+            timeout=self.operation_timeout)
+        return response
+
+    def post(self, address, message, headers):
+        """Proxy to requests.posts()
+
+        :param address: The URL for the request
+        :param message: The content for the body
+        :param headers: a dictionary with the HTTP headers.
+
+        """
+        if self.logger.isEnabledFor(logging.DEBUG):
+            log_message = message
+            if isinstance(log_message, bytes):
+                log_message = log_message.decode('utf-8')
+            self.logger.debug("HTTP Post to %s:\n%s", address, log_message)
+
+        response = self.session.post(
+            address,
+            data=message,
+            headers=headers,
+            timeout=self.operation_timeout)
+
+        if self.logger.isEnabledFor(logging.DEBUG):
+            log_message = response.content
+            if isinstance(log_message, bytes):
+                log_message = log_message.decode('utf-8')
+
+            self.logger.debug(
+                "HTTP Response from %s (status: %d):\n%s",
+                address, response.status_code, log_message)
+
+        return response
+
+    def post_xml(self, address, envelope, headers):
+        """Post the envelope xml element to the given address with the headers.
+
+        This method is intended to be overriden if you want to customize the
+        serialization of the xml element. By default the body is formatted
+        and encoded as utf-8. See ``zeep.wsdl.utils.etree_to_string``.
+
+        """
+        message = etree_to_string(envelope)
+        return self.post(address, message, headers)
+
+    def load(self, url):
+        """Load the content from the given URL"""
+        if not url:
+            raise ValueError("No url given to load")
+
+        scheme = urlparse(url).scheme
+        if scheme in ('http', 'https'):
+
+            if self.cache:
+                response = self.cache.get(url)
+                if response:
+                    return bytes(response)
+
+            content = self._load_remote_data(url)
+
+            if self.cache:
+                self.cache.add(url, content)
+
+            return content
+
+        elif scheme == 'file':
+            if url.startswith('file://'):
+                url = url[7:]
+
+        with open(os.path.expanduser(url), 'rb') as fh:
+            return fh.read()
+
+    def _load_remote_data(self, url):
+        response = self.session.get(url, timeout=self.load_timeout)
+        response.raise_for_status()
+        return response.content
+
+    @contextmanager
+    def _options(self, timeout=None):
+        """Context manager to temporarily overrule options.
+
+        Example::
+
+            client = zeep.Client('foo.wsdl')
+            with client.options(timeout=10):
+                client.service.fast_call()
+
+        :param timeout: Set the timeout for POST/GET operations (not used for
+                        loading external WSDL or XSD documents)
+
+        """
+        old_timeout = self.operation_timeout
+        self.operation_timeout = timeout
+        yield
+        self.operation_timeout = old_timeout
diff --git a/src/zeep/utils.py b/src/zeep/utils.py
new file mode 100644
index 0000000..531c7f2
--- /dev/null
+++ b/src/zeep/utils.py
@@ -0,0 +1,67 @@
+import inspect
+
+
+from lxml import etree
+
+
+class _NotSetClass(object):
+    def __repr__(self):
+        return 'NotSet'
+
+
+NotSet = _NotSetClass()
+
+
+def qname_attr(node, attr_name, target_namespace=None):
+    value = node.get(attr_name)
+    if value is not None:
+        return as_qname(value, node.nsmap, target_namespace)
+
+
+def as_qname(value, nsmap, target_namespace):
+    """Convert the given value to a QName"""
+    if ':' in value:
+        prefix, local = value.split(':')
+        namespace = nsmap.get(prefix, prefix)
+        return etree.QName(namespace, local)
+
+    if target_namespace:
+        return etree.QName(target_namespace, value)
+
+    if nsmap.get(None):
+        return etree.QName(nsmap[None], value)
+    return etree.QName(value)
+
+
+def findall_multiple_ns(node, name, namespace_sets):
+    result = []
+    for nsmap in namespace_sets:
+        result.extend(node.findall(name, namespaces=nsmap))
+    return result
+
+
+def get_version():
+    from zeep import __version__  # cyclic import
+
+    return __version__
+
+
+def get_base_class(objects):
+    """Return the best base class for multiple objects.
+
+    Implementation is quick and dirty, might be done better.. ;-)
+
+    """
+    bases = [inspect.getmro(obj.__class__)[::-1] for obj in objects]
+    num_objects = len(objects)
+    max_mro = max(len(mro) for mro in bases)
+
+    base_class = None
+    for i in range(max_mro):
+        try:
+            if len({bases[j][i] for j in range(num_objects)}) > 1:
+                break
+        except IndexError:
+            break
+        base_class = bases[0][i]
+    return base_class
diff --git a/src/zeep/wsa.py b/src/zeep/wsa.py
new file mode 100644
index 0000000..f406934
--- /dev/null
+++ b/src/zeep/wsa.py
@@ -0,0 +1,42 @@
+import uuid
+
+from lxml import etree
+from lxml.builder import ElementMaker
+
+from zeep.plugins import Plugin
+from zeep.wsdl.utils import get_or_create_header
+
+WSA = ElementMaker(namespace='http://www.w3.org/2005/08/addressing')
+
+
+class WsAddressingPlugin(Plugin):
+    nsmap = {
+        'wsa': 'http://www.w3.org/2005/08/addressing'
+    }
+
+    def egress(self, envelope, http_headers, operation, binding_options):
+        """Apply the ws-addressing headers to the given envelope."""
+
+        wsa_action = operation.input.abstract.wsa_action
+        if not wsa_action:
+            wsa_action = operation.soapaction
+
+        header = get_or_create_header(envelope)
+        headers = [
+            WSA.Action(wsa_action),
+            WSA.MessageID('urn:uuid:' + str(uuid.uuid4())),
+            WSA.To(binding_options['address']),
+        ]
+        header.extend(headers)
+
+        # the top_nsmap kwarg was added in lxml 3.5.0
+        if etree.LXML_VERSION[:2] >= (3, 5):
+            etree.cleanup_namespaces(
+                header,
+                keep_ns_prefixes=header.nsmap,
+                top_nsmap=self.nsmap)
+        else:
+            etree.cleanup_namespaces(
+                header,
+                keep_ns_prefixes=header.nsmap)
+        return envelope, http_headers
diff --git a/src/zeep/wsdl/__init__.py b/src/zeep/wsdl/__init__.py
new file mode 100644
index 0000000..2dbac73
--- /dev/null
+++ b/src/zeep/wsdl/__init__.py
@@ -0,0 +1 @@
+from zeep.wsdl.wsdl import Document  # noqa
diff --git a/src/zeep/wsdl/bindings/__init__.py b/src/zeep/wsdl/bindings/__init__.py
new file mode 100644
index 0000000..ad34dde
--- /dev/null
+++ b/src/zeep/wsdl/bindings/__init__.py
@@ -0,0 +1,2 @@
+from .soap import Soap11Binding, Soap12Binding  # noqa
+from .http import HttpGetBinding, HttpPostBinding  # noqa
diff --git a/src/zeep/wsdl/bindings/http.py b/src/zeep/wsdl/bindings/http.py
new file mode 100644
index 0000000..e5dc8f8
--- /dev/null
+++ b/src/zeep/wsdl/bindings/http.py
@@ -0,0 +1,181 @@
+import logging
+
+import six
+from lxml import etree
+
+from zeep.exceptions import Fault
+from zeep.utils import qname_attr
+from zeep.wsdl import messages
+from zeep.wsdl.definitions import Binding, Operation
+
+logger = logging.getLogger(__name__)
+
+NSMAP = {
+    'http': 'http://schemas.xmlsoap.org/wsdl/http/',
+    'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+    'mime': 'http://schemas.xmlsoap.org/wsdl/mime/',
+}
+
+
+class HttpBinding(Binding):
+
+    def create_message(self, operation, *args, **kwargs):
+        if isinstance(operation, six.string_types):
+            operation = self.get(operation)
+            if not operation:
+                raise ValueError("Operation not found")
+        return operation.create(*args, **kwargs)
+
+    def process_service_port(self, xmlelement, force_https=False):
+        address_node = xmlelement.find('http:address', namespaces=NSMAP)
+        if address_node is None:
+            raise ValueError("No `http:address` node found")
+
+        # Force the usage of HTTPS when the force_https boolean is true
+        location = address_node.get('location')
+        if force_https and location and location.startswith('http://'):
+            logger.warning("Forcing http:address location to HTTPS")
+            location = 'https://' + location[8:]
+
+        return {
+            'address': location
+        }
+
+    @classmethod
+    def parse(cls, definitions, xmlelement):
+        name = qname_attr(xmlelement, 'name', definitions.target_namespace)
+        port_name = qname_attr(xmlelement, 'type', definitions.target_namespace)
+
+        obj = cls(definitions.wsdl, name, port_name)
+        for node in xmlelement.findall('wsdl:operation', namespaces=NSMAP):
+            operation = HttpOperation.parse(definitions, node, obj)
+            obj._operation_add(operation)
+        return obj
+
+    def process_reply(self, client, operation, response):
+        if response.status_code != 200:
+            return self.process_error(response.content)
+            raise NotImplementedError("No error handling yet!")
+        return operation.process_reply(response.content)
+
+    def process_error(self, doc):
+        raise Fault(message=doc)
+
+
+class HttpPostBinding(HttpBinding):
+
+    def send(self, client, options, operation, args, kwargs):
+        """Called from the service"""
+        operation_obj = self.get(operation)
+        if not operation_obj:
+            raise ValueError("Operation %r not found" % operation)
+
+        serialized = operation_obj.create(*args, **kwargs)
+
+        url = options['address'] + serialized.path
+        response = client.transport.post(
+            url, serialized.content, headers=serialized.headers)
+        return self.process_reply(client, operation_obj, response)
+
+    @classmethod
+    def match(cls, node):
+        """Check if this binding instance should be used to parse the given
+        node.
+
+        :param node: The node to match against
+        :type node: lxml.etree._Element
+
+        """
+        http_node = node.find(etree.QName(NSMAP['http'], 'binding'))
+        return http_node is not None and http_node.get('verb') == 'POST'
+
+
+class HttpGetBinding(HttpBinding):
+
+    def send(self, client, options, operation, args, kwargs):
+        """Called from the service"""
+        operation_obj = self.get(operation)
+        if not operation_obj:
+            raise ValueError("Operation %r not found" % operation)
+
+        serialized = operation_obj.create(*args, **kwargs)
+
+        url = options['address'] + serialized.path
+        response = client.transport.get(
+            url, serialized.content, headers=serialized.headers)
+        return self.process_reply(client, operation_obj, response)
+
+    @classmethod
+    def match(cls, node):
+        """Check if this binding instance should be used to parse the given
+        node.
+
+        :param node: The node to match against
+        :type node: lxml.etree._Element
+
+        """
+        http_node = node.find(etree.QName(NSMAP['http'], 'binding'))
+        return http_node is not None and http_node.get('verb') == 'GET'
+
+
+class HttpOperation(Operation):
+    def __init__(self, name, binding, location):
+        super(HttpOperation, self).__init__(name, binding)
+        self.location = location
+
+    def process_reply(self, envelope):
+        return self.output.deserialize(envelope)
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, binding):
+        """
+
+            <wsdl:operation name="GetLastTradePrice">
+              <http:operation location="GetLastTradePrice"/>
+              <wsdl:input>
+                <mime:content type="application/x-www-form-urlencoded"/>
+              </wsdl:input>
+              <wsdl:output>
+                <mime:mimeXml/>
+              </wsdl:output>
+            </wsdl:operation>
+
+        """
+        name = xmlelement.get('name')
+
+        http_operation = xmlelement.find('http:operation', namespaces=NSMAP)
+        location = http_operation.get('location')
+        obj = cls(name, binding, location)
+
+        for node in xmlelement.getchildren():
+            tag_name = etree.QName(node.tag).localname
+            if tag_name not in ('input', 'output'):
+                continue
+
+            # XXX Multiple mime types may be declared as alternatives
+            message_node = None
+            if len(node.getchildren()) > 0:
+                message_node = node.getchildren()[0]
+            message_class = None
+            if message_node is not None:
+                if message_node.tag == etree.QName(NSMAP['http'], 'urlEncoded'):
+                    message_class = messages.UrlEncoded
+                elif message_node.tag == etree.QName(NSMAP['http'], 'urlReplacement'):
+                    message_class = messages.UrlReplacement
+                elif message_node.tag == etree.QName(NSMAP['mime'], 'content'):
+                    message_class = messages.MimeContent
+                elif message_node.tag == etree.QName(NSMAP['mime'], 'mimeXml'):
+                    message_class = messages.MimeXML
+
+            if message_class:
+                msg = message_class.parse(definitions, node, obj)
+                assert msg
+                setattr(obj, tag_name, msg)
+        return obj
+
+    def resolve(self, definitions):
+        super(HttpOperation, self).resolve(definitions)
+        if self.output:
+            self.output.resolve(definitions, self.abstract.output_message)
+        if self.input:
+            self.input.resolve(definitions, self.abstract.input_message)
diff --git a/src/zeep/wsdl/bindings/soap.py b/src/zeep/wsdl/bindings/soap.py
new file mode 100644
index 0000000..b700434
--- /dev/null
+++ b/src/zeep/wsdl/bindings/soap.py
@@ -0,0 +1,387 @@
+import logging
+
+from lxml import etree
+
+from zeep import plugins, wsa
+from zeep.exceptions import Fault, TransportError, XMLSyntaxError
+from zeep.parser import parse_xml
+from zeep.utils import as_qname, qname_attr
+from zeep.wsdl.definitions import Binding, Operation
+from zeep.wsdl.messages import DocumentMessage, RpcMessage
+from zeep.wsdl.utils import etree_to_string
+
+logger = logging.getLogger(__name__)
+
+
+class SoapBinding(Binding):
+    """Soap 1.1/1.2 binding"""
+
+    def __init__(self, wsdl, name, port_name, transport, default_style):
+        """The SoapBinding is the base class for the Soap11Binding and
+        Soap12Binding.
+
+        :param wsdl:
+        :type wsdl:
+        :param name:
+        :type name: string
+        :param port_name:
+        :type port_name: string
+        :param transport:
+        :type transport: zeep.transports.Transport
+        :param default_style:
+
+        """
+        super(SoapBinding, self).__init__(wsdl, name, port_name)
+        self.transport = transport
+        self.default_style = default_style
+
+    @classmethod
+    def match(cls, node):
+        """Check if this binding instance should be used to parse the given
+        node.
+
+        :param node: The node to match against
+        :type node: lxml.etree._Element
+
+        """
+        soap_node = node.find('soap:binding', namespaces=cls.nsmap)
+        return soap_node is not None
+
+    def create_message(self, operation, *args, **kwargs):
+        envelope, http_headers = self._create(operation, args, kwargs)
+        return envelope
+
+    def _create(self, operation, args, kwargs, client=None, options=None):
+        """Create the XML document to send to the server.
+
+        Note that this generates the soap envelope without the wsse applied.
+
+        """
+        operation_obj = self.get(operation)
+        if not operation_obj:
+            raise ValueError("Operation %r not found" % operation)
+
+        # Create the SOAP envelope
+        serialized = operation_obj.create(*args, **kwargs)
+        self._set_http_headers(serialized, operation_obj)
+
+        envelope = serialized.content
+        http_headers = serialized.headers
+
+        # Apply ws-addressing
+        if client:
+            if not options:
+                options = client.service._binding_options
+
+            if operation_obj.abstract.input_message.wsa_action:
+                envelope, http_headers = wsa.WsAddressingPlugin().egress(
+                    envelope, http_headers, operation_obj, options)
+
+            # Apply plugins
+            envelope, http_headers = plugins.apply_egress(
+                client, envelope, http_headers, operation_obj, options)
+
+            # Apply WSSE
+            if client.wsse:
+                envelope, http_headers = client.wsse.sign(envelope, http_headers)
+        return envelope, http_headers
+
+    def send(self, client, options, operation, args, kwargs):
+        """Called from the service
+
+        :param client: The client with which the operation was called
+        :type client: zeep.client.Client
+        :param options: The binding options
+        :type options: dict
+        :param operation: The operation object from which this is a reply
+        :type operation: zeep.wsdl.definitions.Operation
+        :param args: The *args to pass to the operation
+        :type args: tuple
+        :param kwargs: The **kwargs to pass to the operation
+        :type kwargs: dict
+
+        """
+        envelope, http_headers = self._create(
+            operation, args, kwargs,
+            client=client,
+            options=options)
+
+        response = client.transport.post_xml(
+            options['address'], envelope, http_headers)
+
+        operation_obj = self.get(operation)
+        return self.process_reply(client, operation_obj, response)
+
+    def process_reply(self, client, operation, response):
+        """Process the XML reply from the server.
+
+        :param client: The client with which the operation was called
+        :type client: zeep.client.Client
+        :param operation: The operation object from which this is a reply
+        :type operation: zeep.wsdl.definitions.Operation
+        :param response: The response object returned by the remote server
+        :type response: requests.Response
+
+        """
+        if response.status_code != 200 and not response.content:
+            raise TransportError(
+                u'Server returned HTTP status %d (no content available)'
+                % response.status_code)
+
+        try:
+            doc = parse_xml(response.content, recover=True)
+        except XMLSyntaxError:
+            raise TransportError(
+                u'Server returned HTTP status %d (%s)'
+                % (response.status_code, response.content))
+
+        if client.wsse:
+            client.wsse.verify(doc)
+
+        doc, http_headers = plugins.apply_ingress(
+            client, doc, response.headers, operation)
+
+        # If the response code is not 200 or if there is a Fault node available
+        # then assume that an error occured.
+        fault_node = doc.find(
+            'soap-env:Body/soap-env:Fault', namespaces=self.nsmap)
+        if response.status_code != 200 or fault_node is not None:
+            return self.process_error(doc, operation)
+
+        return operation.process_reply(doc)
+
+    def process_error(self, doc, operation):
+        raise NotImplementedError
+
+    def process_service_port(self, xmlelement, force_https=False):
+        address_node = xmlelement.find('soap:address', namespaces=self.nsmap)
+
+        # Force the usage of HTTPS when the force_https boolean is true
+        location = address_node.get('location')
+        if force_https and location and location.startswith('http://'):
+            logger.warning("Forcing soap:address location to HTTPS")
+            location = 'https://' + location[7:]
+
+        return {
+            'address': location
+        }
+
+    @classmethod
+    def parse(cls, definitions, xmlelement):
+        """
+            <wsdl:binding name="nmtoken" type="qname"> *
+                <-- extensibility element (1) --> *
+                <wsdl:operation name="nmtoken"> *
+                   <-- extensibility element (2) --> *
+                   <wsdl:input name="nmtoken"? > ?
+                       <-- extensibility element (3) -->
+                   </wsdl:input>
+                   <wsdl:output name="nmtoken"? > ?
+                       <-- extensibility element (4) --> *
+                   </wsdl:output>
+                   <wsdl:fault name="nmtoken"> *
+                       <-- extensibility element (5) --> *
+                   </wsdl:fault>
+                </wsdl:operation>
+            </wsdl:binding>
+        """
+        name = qname_attr(xmlelement, 'name', definitions.target_namespace)
+        port_name = qname_attr(xmlelement, 'type', definitions.target_namespace)
+
+        # The soap:binding element contains the transport method and
+        # default style attribute for the operations.
+        soap_node = xmlelement.find('soap:binding', namespaces=cls.nsmap)
+        transport = soap_node.get('transport')
+        if transport != 'http://schemas.xmlsoap.org/soap/http':
+            raise NotImplementedError("Only soap/http is supported for now")
+        default_style = soap_node.get('style', 'document')
+
+        obj = cls(definitions.wsdl, name, port_name, transport, default_style)
+        for node in xmlelement.findall('wsdl:operation', namespaces=cls.nsmap):
+            operation = SoapOperation.parse(definitions, node, obj, nsmap=cls.nsmap)
+            obj._operation_add(operation)
+        return obj
+
+
+class Soap11Binding(SoapBinding):
+    nsmap = {
+        'soap': 'http://schemas.xmlsoap.org/wsdl/soap/',
+        'soap-env': 'http://schemas.xmlsoap.org/soap/envelope/',
+        'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+        'xsd': 'http://www.w3.org/2001/XMLSchema',
+    }
+
+    def process_error(self, doc, operation):
+        fault_node = doc.find(
+            'soap-env:Body/soap-env:Fault', namespaces=self.nsmap)
+
+        if fault_node is None:
+            raise Fault(
+                message='Unknown fault occured',
+                code=None,
+                actor=None,
+                detail=etree_to_string(doc))
+
+        def get_text(name):
+            child = fault_node.find(name)
+            if child is not None:
+                return child.text
+
+        raise Fault(
+            message=get_text('faultstring'),
+            code=get_text('faultcode'),
+            actor=get_text('faultactor'),
+            detail=fault_node.find('detail'))
+
+    def _set_http_headers(self, serialized, operation):
+        serialized.headers['Content-Type'] = 'text/xml; charset=utf-8'
+
+
+class Soap12Binding(SoapBinding):
+    nsmap = {
+        'soap': 'http://schemas.xmlsoap.org/wsdl/soap12/',
+        'soap-env': 'http://www.w3.org/2003/05/soap-envelope',
+        'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+        'xsd': 'http://www.w3.org/2001/XMLSchema',
+    }
+
+    def process_error(self, doc, operation):
+        fault_node = doc.find(
+            'soap-env:Body/soap-env:Fault', namespaces=self.nsmap)
+
+        if fault_node is None:
+            raise Fault(
+                message='Unknown fault occured',
+                code=None,
+                actor=None,
+                detail=etree_to_string(doc))
+
+        def get_text(name):
+            child = fault_node.find(name)
+            if child is not None:
+                return child.text
+
+        message = fault_node.findtext('soap-env:Reason/soap-env:Text', namespaces=self.nsmap)
+        code = fault_node.findtext('soap-env:Code/soap-env:Value', namespaces=self.nsmap)
+
+        # Extract the fault subcodes. These can be nested, as in subcodes can
+        # also contain other subcodes.
+        subcodes = []
+        subcode_element = fault_node.find('soap-env:Code/soap-env:Subcode', namespaces=self.nsmap)
+        while subcode_element is not None:
+            subcode_value_element = subcode_element.find('soap-env:Value', namespaces=self.nsmap)
+            subcode_qname = as_qname(subcode_value_element.text, subcode_value_element.nsmap, None)
+            subcodes.append(subcode_qname)
+            subcode_element = subcode_element.find('soap-env:Subcode', namespaces=self.nsmap)
+
+        # TODO: We should use the fault message as defined in the wsdl.
+        detail_node = fault_node.find('soap-env:Detail', namespaces=self.nsmap)
+        raise Fault(
+            message=message,
+            code=code,
+            actor=None,
+            detail=detail_node,
+            subcodes=subcodes)
+
+    def _set_http_headers(self, serialized, operation):
+        serialized.headers['Content-Type'] = '; '.join([
+            'application/soap+xml',
+            'charset=utf-8',
+            'action="%s"' % operation.soapaction
+        ])
+
+
+class SoapOperation(Operation):
+    """Represent's an operation within a specific binding."""
+
+    def __init__(self, name, binding, nsmap, soapaction, style):
+        super(SoapOperation, self).__init__(name, binding)
+        self.nsmap = nsmap
+        self.soapaction = soapaction
+        self.style = style
+
+    def process_reply(self, envelope):
+        envelope_qname = etree.QName(self.nsmap['soap-env'], 'Envelope')
+        if envelope.tag != envelope_qname:
+            raise XMLSyntaxError((
+                "The XML returned by the server does not contain a valid " +
+                "{%s}Envelope root element. The root element found is %s "
+            ) % (envelope_qname.namespace, envelope.tag))
+
+        return self.output.deserialize(envelope)
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, binding, nsmap):
+        """
+
+            <wsdl:operation name="nmtoken"> *
+                <soap:operation soapAction="uri"? style="rpc|document"?>?
+                <wsdl:input name="nmtoken"? > ?
+                    <soap:body use="literal"/>
+               </wsdl:input>
+               <wsdl:output name="nmtoken"? > ?
+                    <-- extensibility element (4) --> *
+               </wsdl:output>
+               <wsdl:fault name="nmtoken"> *
+                    <-- extensibility element (5) --> *
+               </wsdl:fault>
+            </wsdl:operation>
+
+        Example::
+
+            <wsdl:operation name="GetLastTradePrice">
+              <soap:operation soapAction="http://example.com/GetLastTradePrice"/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+              <wsdl:output>
+              </wsdl:output>
+              <wsdl:fault name="dataFault">
+                <soap:fault name="dataFault" use="literal"/>
+              </wsdl:fault>
+            </operation>
+
+        """
+        name = xmlelement.get('name')
+
+        # The soap:operation element is required for soap/http bindings
+        # and may be omitted for other bindings.
+        soap_node = xmlelement.find('soap:operation', namespaces=binding.nsmap)
+        action = None
+        if soap_node is not None:
+            action = soap_node.get('soapAction')
+            style = soap_node.get('style', binding.default_style)
+        else:
+            style = binding.default_style
+
+        obj = cls(name, binding, nsmap, action, style)
+
+        if style == 'rpc':
+            message_class = RpcMessage
+        else:
+            message_class = DocumentMessage
+
+        for node in xmlelement.getchildren():
+            tag_name = etree.QName(node.tag).localname
+            if tag_name not in ('input', 'output', 'fault'):
+                continue
+            msg = message_class.parse(
+                definitions=definitions, xmlelement=node,
+                operation=obj, nsmap=nsmap, type=tag_name)
+            if tag_name == 'fault':
+                obj.faults[msg.name] = msg
+            else:
+                setattr(obj, tag_name, msg)
+
+        return obj
+
+    def resolve(self, definitions):
+        super(SoapOperation, self).resolve(definitions)
+        for name, fault in self.faults.items():
+            if name in self.abstract.fault_messages:
+                fault.resolve(definitions, self.abstract.fault_messages[name])
+
+        if self.output:
+            self.output.resolve(definitions, self.abstract.output_message)
+        if self.input:
+            self.input.resolve(definitions, self.abstract.input_message)
diff --git a/src/zeep/wsdl/definitions.py b/src/zeep/wsdl/definitions.py
new file mode 100644
index 0000000..b01e1c2
--- /dev/null
+++ b/src/zeep/wsdl/definitions.py
@@ -0,0 +1,264 @@
+from collections import OrderedDict, namedtuple
+
+from six import python_2_unicode_compatible
+
+MessagePart = namedtuple('MessagePart', ['element', 'type'])
+
+
+class AbstractMessage(object):
+    """Messages consist of one or more logical parts.
+
+    Each part is associated with a type from some type system using a
+    message-typing attribute. The set of message-typing attributes is
+    extensible. WSDL defines several such message-typing attributes for use
+    with XSD:
+
+        element: Refers to an XSD element using a QName.
+        type: Refers to an XSD simpleType or complexType using a QName.
+
+    """
+    def __init__(self, name):
+        self.name = name
+        self.parts = OrderedDict()
+
+    def __repr__(self):
+        return '<%s(name=%r)>' % (self.__class__.__name__, self.name.text)
+
+    def resolve(self, definitions):
+        pass
+
+    def add_part(self, name, element):
+        self.parts[name] = element
+
+
+class AbstractOperation(object):
+    """Abstract operations are defined in the wsdl's portType elements."""
+
+    def __init__(self, name, input_message=None, output_message=None,
+                 fault_messages=None, parameter_order=None):
+        """Initialize the abstract operation.
+
+        :param name: The name of the operation
+        :type name: str
+        :param input_message: Message to generate the request XML
+        :type input_message: AbstractMessage
+        :param output_message: Message to process the response XML
+        :type output_message: AbstractMessage
+        :param fault_messages: Dict of messages to handle faults
+        :type fault_messages: dict of str: AbstractMessage
+
+        """
+        self.name = name
+        self.input_message = input_message
+        self.output_message = output_message
+        self.fault_messages = fault_messages
+        self.parameter_order = parameter_order
+
+
+class PortType(object):
+    def __init__(self, name, operations):
+        self.name = name
+        self.operations = operations
+
+    def __repr__(self):
+        return '<%s(name=%r)>' % (
+            self.__class__.__name__, self.name.text)
+
+    def resolve(self, definitions):
+        pass
+
+
+ at python_2_unicode_compatible
+class Binding(object):
+    """Base class for the various bindings (SoapBinding / HttpBinding)
+
+        Binding
+           |
+           +-> Operation
+                   |
+                   +-> ConcreteMessage
+                             |
+                             +-> AbstractMessage
+
+    """
+    def __init__(self, wsdl, name, port_name):
+        """Binding
+
+        :param wsdl:
+        :type wsdl:
+        :param name:
+        :type name: string
+        :param port_name:
+        :type port_name: string
+
+        """
+        self.name = name
+        self.port_name = port_name
+        self.port_type = None
+        self.wsdl = wsdl
+        self._operations = {}
+
+    def resolve(self, definitions):
+        self.port_type = definitions.get('port_types', self.port_name.text)
+        for operation in self._operations.values():
+            operation.resolve(definitions)
+
+    def _operation_add(self, operation):
+        # XXX: operation name is not unique
+        self._operations[operation.name] = operation
+
+    def __str__(self):
+        return '%s: %s' % (self.__class__.__name__, self.name.text)
+
+    def __repr__(self):
+        return '<%s(name=%r, port_type=%r)>' % (
+            self.__class__.__name__, self.name.text, self.port_type)
+
+    def get(self, key):
+        try:
+            return self._operations[key]
+        except KeyError:
+            raise ValueError("No such operation %r on %s" % (key, self.name))
+
+    @classmethod
+    def match(cls, node):
+        raise NotImplementedError()
+
+    @classmethod
+    def parse(cls, definitions, xmlelement):
+        raise NotImplementedError()
+
+
+ at python_2_unicode_compatible
+class Operation(object):
+    """Concrete operation
+
+    Contains references to the concrete messages
+
+    """
+    def __init__(self, name, binding):
+        self.name = name
+        self.binding = binding
+        self.abstract = None
+        self.style = None
+        self.input = None
+        self.output = None
+        self.faults = {}
+
+    def resolve(self, definitions):
+        self.abstract = self.binding.port_type.operations[self.name]
+
+    def __repr__(self):
+        return '<%s(name=%r, style=%r)>' % (
+            self.__class__.__name__, self.name, self.style)
+
+    def __str__(self):
+        if not self.input:
+            return u'%s(missing input message)' % (self.name)
+
+        retval = u'%s(%s)' % (self.name, self.input.signature())
+        if self.output:
+            retval += u' -> %s' % (self.output.signature(as_output=True))
+        return retval
+
+    def create(self, *args, **kwargs):
+        return self.input.serialize(*args, **kwargs)
+
+    def process_reply(self, envelope):
+        raise NotImplementedError()
+
+    @classmethod
+    def parse(cls, wsdl, xmlelement, binding):
+        """
+            <wsdl:operation name="nmtoken"> *
+               <-- extensibility element (2) --> *
+               <wsdl:input name="nmtoken"? > ?
+                   <-- extensibility element (3) -->
+               </wsdl:input>
+               <wsdl:output name="nmtoken"? > ?
+                   <-- extensibility element (4) --> *
+               </wsdl:output>
+               <wsdl:fault name="nmtoken"> *
+                   <-- extensibility element (5) --> *
+               </wsdl:fault>
+            </wsdl:operation>
+        """
+        raise NotImplementedError()
+
+
+ at python_2_unicode_compatible
+class Port(object):
+    def __init__(self, name, binding_name, xmlelement):
+        self.name = name
+        self._resolve_context = {
+            'binding_name': binding_name,
+            'xmlelement': xmlelement,
+        }
+
+        # Set during resolve()
+        self.binding = None
+        self.binding_options = None
+
+    def __repr__(self):
+        return '<%s(name=%r, binding=%r, %r)>' % (
+            self.__class__.__name__, self.name, self.binding,
+            self.binding_options)
+
+    def __str__(self):
+        return u'Port: %s (%s)' % (self.name, self.binding)
+
+    def resolve(self, definitions):
+        if self._resolve_context is None:
+            return
+
+        try:
+            self.binding = definitions.get(
+                'bindings', self._resolve_context['binding_name'].text)
+        except IndexError:
+            return False
+
+        if definitions.location:
+            force_https = definitions.location.startswith('https')
+        else:
+            force_https = False
+
+        self.binding_options = self.binding.process_service_port(
+            self._resolve_context['xmlelement'],
+            force_https)
+        self._resolve_context = None
+        return True
+
+
+ at python_2_unicode_compatible
+class Service(object):
+
+    def __init__(self, name):
+        self.ports = OrderedDict()
+        self.name = name
+        self._is_resolved = False
+
+    def __str__(self):
+        return u'Service: %s' % self.name
+
+    def __repr__(self):
+        return '<%s(name=%r, ports=%r)>' % (
+            self.__class__.__name__, self.name, self.ports)
+
+    def resolve(self, definitions):
+        if self._is_resolved:
+            return
+
+        unresolved = []
+        for name, port in self.ports.items():
+            is_resolved = port.resolve(definitions)
+            if not is_resolved:
+                unresolved.append(name)
+
+        # Remove unresolved bindings (http etc)
+        for name in unresolved:
+            del self.ports[name]
+
+        self._is_resolved = True
+
+    def add_port(self, port):
+        self.ports[port.name] = port
diff --git a/src/zeep/wsdl/messages/__init__.py b/src/zeep/wsdl/messages/__init__.py
new file mode 100644
index 0000000..70a5b5a
--- /dev/null
+++ b/src/zeep/wsdl/messages/__init__.py
@@ -0,0 +1,3 @@
+from .http import *  # noqa
+from .mime import *  # noqa
+from .soap import *  # noqa
diff --git a/src/zeep/wsdl/messages/base.py b/src/zeep/wsdl/messages/base.py
new file mode 100644
index 0000000..cc71e93
--- /dev/null
+++ b/src/zeep/wsdl/messages/base.py
@@ -0,0 +1,47 @@
+from collections import namedtuple
+
+from zeep import xsd
+
+SerializedMessage = namedtuple(
+    'SerializedMessage', ['path', 'headers', 'content'])
+
+
+class ConcreteMessage(object):
+    """Represents the wsdl:binding -> wsdl:operation -> input/ouput node"""
+    def __init__(self, wsdl, name, operation):
+        assert wsdl
+        assert operation
+
+        self.wsdl = wsdl
+        self.namespace = {}
+        self.operation = operation
+        self.name = name
+
+    def serialize(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    def deserialize(self, node):
+        raise NotImplementedError()
+
+    def signature(self, as_output=False):
+        if not self.body:
+            return None
+
+        if as_output:
+            if isinstance(self.body.type, xsd.ComplexType):
+                try:
+                    if len(self.body.type.elements) == 1:
+                        return self.body.type.elements[0][1].type.signature()
+                except AttributeError:
+                    return None
+
+            return self.body.type.signature()
+
+        parts = [self.body.type.signature()]
+        if getattr(self, 'header', None):
+            parts.append('_soapheaders={%s}' % self.header.signature())
+        return ', '.join(part for part in parts if part)
+
+    @classmethod
+    def parse(cls, wsdl, xmlelement, abstract_message, operation):
+        raise NotImplementedError()
diff --git a/src/zeep/wsdl/messages/http.py b/src/zeep/wsdl/messages/http.py
new file mode 100644
index 0000000..6264dca
--- /dev/null
+++ b/src/zeep/wsdl/messages/http.py
@@ -0,0 +1,91 @@
+from zeep import xsd
+from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage
+
+__all__ = [
+    'UrlEncoded',
+    'UrlReplacement',
+]
+
+
+class HttpMessage(ConcreteMessage):
+    """Base class for HTTP Binding messages"""
+
+    def resolve(self, definitions, abstract_message):
+        self.abstract = abstract_message
+
+        children = []
+        for name, message in self.abstract.parts.items():
+            if message.element:
+                elm = message.element.clone(name)
+            else:
+                elm = xsd.Element(name, message.type)
+            children.append(elm)
+        self.body = xsd.Element(
+            self.operation.name, xsd.ComplexType(xsd.Sequence(children)))
+
+
+class UrlEncoded(HttpMessage):
+    """The urlEncoded element indicates that all the message parts are encoded
+    into the HTTP request URI using the standard URI-encoding rules
+    (name1=value&name2=value...).
+
+    The names of the parameters correspond to the names of the message parts.
+    Each value contributed by the part is encoded using a name=value pair. This
+    may be used with GET to specify URL encoding, or with POST to specify a
+    FORM-POST. For GET, the "?" character is automatically appended as
+    necessary.
+
+    """
+
+    def serialize(self, *args, **kwargs):
+        params = {key: None for key in self.abstract.parts.keys()}
+        params.update(zip(self.abstract.parts.keys(), args))
+        params.update(kwargs)
+        headers = {'Content-Type': 'text/xml; charset=utf-8'}
+        return SerializedMessage(
+            path=self.operation.location, headers=headers, content=params)
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, operation):
+        name = xmlelement.get('name')
+        obj = cls(definitions.wsdl, name, operation)
+        return obj
+
+
+class UrlReplacement(HttpMessage):
+    """The http:urlReplacement element indicates that all the message parts
+    are encoded into the HTTP request URI using a replacement algorithm.
+
+    - The relative URI value of http:operation is searched for a set of search
+      patterns.
+    - The search occurs before the value of the http:operation is combined with
+      the value of the location attribute from http:address.
+    - There is one search pattern for each message part. The search pattern
+      string is the name of the message part surrounded with parenthesis "("
+      and ")".
+    - For each match, the value of the corresponding message part is
+      substituted for the match at the location of the match.
+    - Matches are performed before any values are replaced (replaced values do
+      not trigger additional matches).
+
+    Message parts MUST NOT have repeating values.
+    <http:urlReplacement/>
+
+    """
+
+    def serialize(self, *args, **kwargs):
+        params = {key: None for key in self.abstract.parts.keys()}
+        params.update(zip(self.abstract.parts.keys(), args))
+        params.update(kwargs)
+        headers = {'Content-Type': 'text/xml; charset=utf-8'}
+
+        path = self.operation.location
+        for key, value in params.items():
+            path = path.replace('(%s)' % key, value if value is not None else '')
+        return SerializedMessage(path=path, headers=headers, content='')
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, operation):
+        name = xmlelement.get('name')
+        obj = cls(definitions.wsdl, name, operation)
+        return obj
diff --git a/src/zeep/wsdl/messages/mime.py b/src/zeep/wsdl/messages/mime.py
new file mode 100644
index 0000000..ba28522
--- /dev/null
+++ b/src/zeep/wsdl/messages/mime.py
@@ -0,0 +1,174 @@
+import six
+from defusedxml.lxml import fromstring
+from lxml import etree
+
+from zeep import xsd
+from zeep.helpers import serialize_object
+from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage
+from zeep.wsdl.utils import etree_to_string
+
+__all__ = [
+    'MimeContent',
+    'MimeXML',
+    'MimeMultipart',
+]
+
+
+class MimeMessage(ConcreteMessage):
+    _nsmap = {
+        'mime': 'http://schemas.xmlsoap.org/wsdl/mime/',
+    }
+
+    def __init__(self, wsdl, name, operation, part_name):
+        super(MimeMessage, self).__init__(wsdl, name, operation)
+        self.part_name = part_name
+
+    def resolve(self, definitions, abstract_message):
+        """Resolve the body element
+
+        The specs are (again) not really clear how to handle the message
+        parts in relation the message element vs type. The following strategy
+        is chosen, which seem to work:
+
+         - If the message part has a name and it maches then set it as body
+         - If the message part has a name but it doesn't match but there are no
+           other message parts, then just use that one.
+         - If the message part has no name then handle it like an rpc call,
+           in other words, each part is an argument.
+
+        """
+        self.abstract = abstract_message
+        if self.part_name and self.abstract.parts:
+            if self.part_name in self.abstract.parts:
+                message = self.abstract.parts[self.part_name]
+            elif len(self.abstract.parts) == 1:
+                message = list(self.abstract.parts.values())[0]
+            else:
+                raise ValueError(
+                    "Multiple parts for message %r while no matching part found" % self.part_name)
+
+            if message.element:
+                self.body = message.element
+            else:
+                elm = xsd.Element(self.part_name, message.type)
+                self.body = xsd.Element(
+                    self.operation.name, xsd.ComplexType(xsd.Sequence([elm])))
+        else:
+            children = []
+            for name, message in self.abstract.parts.items():
+                if message.element:
+                    elm = message.element.clone(name)
+                else:
+                    elm = xsd.Element(name, message.type)
+                children.append(elm)
+            self.body = xsd.Element(
+                self.operation.name, xsd.ComplexType(xsd.Sequence(children)))
+
+
+class MimeContent(MimeMessage):
+    """WSDL includes a way to bind abstract types to concrete messages in some
+    MIME format.
+
+    Bindings for the following MIME types are defined:
+
+    - multipart/related
+    - text/xml
+    - application/x-www-form-urlencoded
+    - Others (by specifying the MIME type string)
+
+    The set of defined MIME types is both large and evolving, so it is not a
+    goal for WSDL to exhaustively define XML grammar for each MIME type.
+
+    """
+    def __init__(self, wsdl, name, operation, content_type, part_name):
+        super(MimeContent, self).__init__(wsdl, name, operation, part_name)
+        self.content_type = content_type
+
+    def serialize(self, *args, **kwargs):
+        value = self.body(*args, **kwargs)
+        headers = {
+            'Content-Type': self.content_type
+        }
+
+        data = ''
+        if self.content_type == 'application/x-www-form-urlencoded':
+            items = serialize_object(value)
+            data = six.moves.urllib.parse.urlencode(items)
+        elif self.content_type == 'text/xml':
+            document = etree.Element('root')
+            self.body.render(document, value)
+            data = etree_to_string(document.getchildren()[0])
+
+        return SerializedMessage(
+            path=self.operation.location, headers=headers, content=data)
+
+    def deserialize(self, node):
+        node = fromstring(node)
+        part = list(self.abstract.parts.values())[0]
+        return part.type.parse_xmlelement(node)
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, operation):
+        name = xmlelement.get('name')
+
+        part_name = content_type = None
+        content_node = xmlelement.find('mime:content', namespaces=cls._nsmap)
+        if content_node is not None:
+            content_type = content_node.get('type')
+            part_name = content_node.get('part')
+
+        obj = cls(definitions.wsdl, name, operation, content_type, part_name)
+        return obj
+
+
+class MimeXML(MimeMessage):
+    """To specify XML payloads that are not SOAP compliant (do not have a SOAP
+    Envelope), but do have a particular schema, the mime:mimeXml element may be
+    used to specify that concrete schema.
+
+    The part attribute refers to a message part defining the concrete schema of
+    the root XML element. The part attribute MAY be omitted if the message has
+    only a single part. The part references a concrete schema using the element
+    attribute for simple parts or type attribute for composite parts
+
+    """
+    def serialize(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    def deserialize(self, node):
+        node = fromstring(node)
+        part = next(iter(self.abstract.parts.values()), None)
+        return part.element.parse(node, self.wsdl.types)
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, operation):
+        name = xmlelement.get('name')
+        part_name = None
+
+        content_node = xmlelement.find('mime:mimeXml', namespaces=cls._nsmap)
+        if content_node is not None:
+            part_name = content_node.get('part')
+        obj = cls(definitions.wsdl, name, operation, part_name)
+        return obj
+
+
+class MimeMultipart(MimeMessage):
+    """The multipart/related MIME type aggregates an arbitrary set of MIME
+    formatted parts into one message using the MIME type "multipart/related".
+
+    The mime:multipartRelated element describes the concrete format of such a
+    message::
+
+        <mime:multipartRelated>
+            <mime:part> *
+                <-- mime element -->
+            </mime:part>
+        </mime:multipartRelated>
+
+    The mime:part element describes each part of a multipart/related message.
+    MIME elements appear within mime:part to specify the concrete MIME type for
+    the part. If more than one MIME element appears inside a mime:part, they
+    are alternatives.
+
+    """
+    pass
diff --git a/src/zeep/wsdl/messages/soap.py b/src/zeep/wsdl/messages/soap.py
new file mode 100644
index 0000000..b1b64b6
--- /dev/null
+++ b/src/zeep/wsdl/messages/soap.py
@@ -0,0 +1,437 @@
+import copy
+from collections import OrderedDict
+
+from lxml import etree
+from lxml.builder import ElementMaker
+
+from zeep import exceptions, xsd
+from zeep.utils import as_qname
+from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage
+
+__all__ = [
+    'DocumentMessage',
+    'RpcMessage',
+]
+
+
+class SoapMessage(ConcreteMessage):
+    """Base class for the SOAP Document and RPC messages"""
+
+    def __init__(self, wsdl, name, operation, type, nsmap):
+        super(SoapMessage, self).__init__(wsdl, name, operation)
+        self.nsmap = nsmap
+        self.abstract = None  # Set during resolve()
+        self.type = type
+
+        self.body = None
+        self.header = None
+        self.envelope = None
+
+    def serialize(self, *args, **kwargs):
+        """Create a SerializedMessage for this message"""
+        nsmap = {
+            'soap-env': self.nsmap['soap-env']
+        }
+        nsmap.update(self.wsdl.types._prefix_map_custom)
+
+        soap = ElementMaker(namespace=self.nsmap['soap-env'], nsmap=nsmap)
+        body = header = None
+
+        # Create the soap:header element
+        headers_value = kwargs.pop('_soapheaders', None)
+        header = self._serialize_header(headers_value, nsmap)
+
+        # Create the soap:body element
+        if self.body:
+            body_value = self.body(*args, **kwargs)
+            body = soap.Body()
+            self.body.render(body, body_value)
+
+        # Create the soap:envelope
+        envelope = soap.Envelope()
+        if header is not None:
+            envelope.append(header)
+        if body is not None:
+            envelope.append(body)
+
+        # XXX: This is only used in Soap 1.1 so should be moved to the the
+        # Soap11Binding._set_http_headers(). But let's keep it like this for
+        # now.
+        headers = {
+            'SOAPAction': '"%s"' % self.operation.soapaction
+        }
+        return SerializedMessage(
+            path=None, headers=headers, content=envelope)
+
+    def deserialize(self, envelope):
+        """Deserialize the SOAP:Envelope and return a CompoundValue with the
+        result.
+
+        """
+        if not self.envelope:
+            return None
+
+        body = envelope.find('soap-env:Body', namespaces=self.nsmap)
+        body_result = self._deserialize_body(body)
+
+        header = envelope.find('soap-env:Header', namespaces=self.nsmap)
+        headers_result = self._deserialize_headers(header)
+
+        kwargs = body_result
+        kwargs.update(headers_result)
+        result = self.envelope(**kwargs)
+
+        # If the message
+        if self.header.type._element:
+            return result
+
+        result = result.body
+        if result is None or len(result) == 0:
+            return None
+        elif len(result) > 1:
+            return result
+
+        # Check if we can remove the wrapping object to make the return value
+        # easier to use.
+        result = next(iter(result.__values__.values()))
+        if isinstance(result, xsd.CompoundValue):
+            children = result._xsd_type.elements
+            if len(children) == 1:
+                item_name, item_element = children[0]
+                retval = getattr(result, item_name)
+                return retval
+        return result
+
+    def signature(self, as_output=False):
+        if not self.envelope:
+            return None
+
+        if as_output:
+            if isinstance(self.envelope.type, xsd.ComplexType):
+                try:
+                    if len(self.envelope.type.elements) == 1:
+                        return self.envelope.type.elements[0][1].type.signature()
+                except AttributeError:
+                    return None
+            return self.envelope.type.signature()
+
+        parts = [self.body.type.signature()]
+        if self.header.type._element:
+            parts.append('_soapheaders={%s}' % self.header.signature())
+        return ', '.join(part for part in parts if part)
+
+    @classmethod
+    def parse(cls, definitions, xmlelement, operation, type, nsmap):
+        """Parse a wsdl:binding/wsdl:operation/wsdl:operation for the SOAP
+        implementation.
+
+        Each wsdl:operation can contain three child nodes:
+         - input
+         - output
+         - fault
+
+        Definition for input/output::
+
+          <input>
+            <soap:body parts="nmtokens"? use="literal|encoded"
+                       encodingStyle="uri-list"? namespace="uri"?>
+
+            <soap:header message="qname" part="nmtoken" use="literal|encoded"
+                         encodingStyle="uri-list"? namespace="uri"?>*
+              <soap:headerfault message="qname" part="nmtoken"
+                                use="literal|encoded"
+                                encodingStyle="uri-list"? namespace="uri"?/>*
+            </soap:header>
+          </input>
+
+        And the definition for fault::
+
+           <soap:fault name="nmtoken" use="literal|encoded"
+                       encodingStyle="uri-list"? namespace="uri"?>
+
+        """
+        name = xmlelement.get('name')
+        obj = cls(definitions.wsdl, name, operation, nsmap=nsmap, type=type)
+
+        body_data = None
+        header_data = None
+
+        body = xmlelement.find('soap:body', namespaces=operation.binding.nsmap)
+        if body is not None:
+            body_data = cls._parse_body(body)
+
+        # Parse soap:header (multiple)
+        elements = xmlelement.findall(
+            'soap:header', namespaces=operation.binding.nsmap)
+        header_data = cls._parse_header(
+            elements, definitions.target_namespace, operation)
+
+        obj._resolve_info = {
+            'body': body_data,
+            'header': header_data
+        }
+        return obj
+
+    @classmethod
+    def _parse_body(cls, xmlelement):
+        """Parse soap:body and return a dict with data to resolve it.
+
+            <soap:body parts="nmtokens"? use="literal|encoded"?
+                       encodingStyle="uri-list"? namespace="uri"?>
+
+        """
+        return {
+            'part': xmlelement.get('part'),
+            'use': xmlelement.get('use', 'literal'),
+            'encodingStyle': xmlelement.get('encodingStyle'),
+            'namespace': xmlelement.get('namespace'),
+        }
+
+    @classmethod
+    def _parse_header(cls, xmlelements, tns, operation):
+        """Parse the soap:header and optionally included soap:headerfault elements
+
+          <soap:header
+            message="qname"
+            part="nmtoken"
+            use="literal|encoded"
+            encodingStyle="uri-list"?
+            namespace="uri"?
+          />*
+
+        The header can optionally contain one ore more soap:headerfault
+        elements which can contain the same attributes as the soap:header::
+
+           <soap:headerfault message="qname" part="nmtoken" use="literal|encoded"
+                             encodingStyle="uri-list"? namespace="uri"?/>*
+
+        """
+        result = []
+        for xmlelement in xmlelements:
+            data = cls._parse_header_element(xmlelement, tns)
+
+            # Add optional soap:headerfault elements
+            data['faults'] = []
+            fault_elements = xmlelement.findall(
+                'soap:headerfault', namespaces=operation.binding.nsmap)
+            for fault_element in fault_elements:
+                fault_data = cls._parse_header_element(fault_element, tns)
+                data['faults'].append(fault_data)
+
+            result.append(data)
+        return result
+
+    @classmethod
+    def _parse_header_element(cls, xmlelement, tns):
+        attributes = xmlelement.attrib
+        message_qname = as_qname(
+            attributes['message'], xmlelement.nsmap, tns)
+
+        try:
+            return {
+                'message': message_qname,
+                'part': attributes['part'],
+                'use': attributes['use'],
+                'encodingStyle': attributes.get('encodingStyle'),
+                'namespace': attributes.get('namespace'),
+            }
+        except KeyError:
+            raise exceptions.WsdlSyntaxError("Invalid soap:header(fault)")
+
+    def resolve(self, definitions, abstract_message):
+        """Resolve the data in the self._resolve_info dict (set via parse())
+
+        This creates three xsd.Element objects:
+
+            - self.header
+            - self.body
+            - self.envelope (combination of headers and body)
+
+        XXX headerfaults are not implemented yet.
+
+        """
+        info = self._resolve_info
+        del self._resolve_info
+
+        # If this message has no parts then we have nothing to do. This might
+        # happen for output messages which don't return anything.
+        if not abstract_message.parts and self.type != 'input':
+            return
+
+        self.abstract = abstract_message
+        parts = OrderedDict(self.abstract.parts)
+
+        self.header = self._resolve_header(info['header'], definitions, parts)
+        self.body = self._resolve_body(info['body'], definitions, parts)
+        self.envelope = self._create_envelope_element()
+
+    def _create_envelope_element(self):
+        """Create combined `envelope` complexType which contains both the
+        elements from the body and the headers.
+
+        """
+        all_elements = xsd.Sequence([
+            xsd.Element('body', self.body.type),
+            xsd.Element('header', self.header.type),
+        ])
+        return xsd.Element('envelope', xsd.ComplexType(all_elements))
+
+    def _serialize_header(self, headers_value, nsmap):
+        if not headers_value:
+            return
+
+        headers_value = copy.deepcopy(headers_value)
+
+        soap = ElementMaker(namespace=self.nsmap['soap-env'], nsmap=nsmap)
+        header = soap.Header()
+        if isinstance(headers_value, list):
+            for header_value in headers_value:
+                if hasattr(header_value, '_xsd_elm'):
+                    header_value._xsd_elm.render(header, header_value)
+                elif isinstance(header_value, etree._Element):
+                    header.append(header_value)
+                else:
+                    raise ValueError("Invalid value given to _soapheaders")
+        elif isinstance(headers_value, dict):
+            if not self.header:
+                raise ValueError(
+                    "_soapheaders only accepts a dictionary if the wsdl "
+                    "defines the headers.")
+            headers_value = self.header(**headers_value)
+            self.header.render(header, headers_value)
+        else:
+            raise ValueError("Invalid value given to _soapheaders")
+
+        return header
+
+    def _deserialize_headers(self, xmlelement):
+        """Deserialize the values in the SOAP:Header element"""
+        if not self.header or xmlelement is None:
+            return {}
+
+        result = self.header.parse(xmlelement, self.wsdl.types)
+        if result is not None:
+            return {'header': result}
+        return {}
+
+    def _resolve_header(self, info, definitions, parts):
+        sequence = xsd.Sequence()
+        if not info:
+            return xsd.Element(None, xsd.ComplexType(sequence))
+
+        for item in info:
+            message_name = item['message'].text
+            part_name = item['part']
+
+            message = definitions.get('messages', message_name)
+            if message == self.abstract:
+                del parts[part_name]
+
+            part = message.parts[part_name]
+            if part.element:
+                element = part.element.clone()
+                element.attr_name = part_name
+            else:
+                element = xsd.Element(part_name, part.type)
+            sequence.append(element)
+        return xsd.Element(None, xsd.ComplexType(sequence))
+
+
+class DocumentMessage(SoapMessage):
+    """In the document message there are no additional wrappers, and the
+    message parts appear directly under the SOAP Body element.
+
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(DocumentMessage, self).__init__(*args, **kwargs)
+        self._is_body_wrapped = False
+
+    def _deserialize_body(self, xmlelement):
+        if self._is_body_wrapped:
+            result = self.body.parse(xmlelement, self.wsdl.types)
+        else:
+            # For now we assume that the body only has one child since only
+            # one part is specified in the wsdl. This should be handled way
+            # better
+            # XXX
+            xmlelement = xmlelement.getchildren()[0]
+            result = self.body.parse(xmlelement, self.wsdl.types)
+        return {'body': result}
+
+    def _resolve_body(self, info, definitions, parts):
+        if not info or not parts:
+            return xsd.Element(None, xsd.ComplexType([]))
+
+        # If the part name is omitted then all parts are available under
+        # the soap:body tag. Otherwise only the part with the given name.
+        if info['part']:
+            part_name = info['part']
+            sub_elements = [parts[part_name].element]
+        else:
+            sub_elements = []
+            for part_name, part in parts.items():
+                element = part.element.clone()
+                element.attr_name = part_name or element.name
+                sub_elements.append(element)
+
+        if len(sub_elements) > 1:
+            self._is_body_wrapped = True
+            return xsd.Element(
+                None, xsd.ComplexType(xsd.All(sub_elements)))
+        else:
+            self._is_body_wrapped = False
+            return sub_elements[0]
+
+
+class RpcMessage(SoapMessage):
+    """In RPC messages each part is a parameter or a return value and appears
+    inside a wrapper element within the body.
+
+    The wrapper element is named identically to the operation name and its
+    namespace is the value of the namespace attribute.  Each message part
+    (parameter) appears under the wrapper, represented by an accessor named
+    identically to the corresponding parameter of the call.  Parts are arranged
+    in the same order as the parameters of the call.
+
+    """
+
+    def _resolve_body(self, info, definitions, parts):
+        """Return an XSD element for the SOAP:Body.
+
+        Each part is a parameter or a return value and appears inside a
+        wrapper element within the body named identically to the operation
+        name and its namespace is the value of the namespace attribute.
+
+        """
+        if not info:
+            return xsd.Element(None, xsd.ComplexType([]))
+
+        namespace = info['namespace']
+        if self.type == 'input':
+            tag_name = etree.QName(namespace, self.operation.name)
+        else:
+            tag_name = etree.QName(namespace, self.abstract.name.localname)
+
+        # Create the xsd element to create/parse the response. Each part
+        # is a sub element of the root node (which uses the operation name)
+        elements = []
+        for name, msg in parts.items():
+            if msg.element:
+                elements.append(msg.element)
+            else:
+                elements.append(xsd.Element(name, msg.type))
+        return xsd.Element(tag_name, xsd.ComplexType(xsd.Sequence(elements)))
+
+    def _deserialize_body(self, body_element):
+        """The name of the wrapper element is not defined. The WS-I defines
+        that it should be the operation name with the 'Response' string as
+        suffix. But lets just do it really stupid for now and use the first
+        element.
+
+        """
+        response_element = body_element.getchildren()[0]
+        if self.body:
+            result = self.body.parse(response_element, self.wsdl.types)
+            return {'body': result}
+        return {'body': None}
diff --git a/src/zeep/wsdl/parse.py b/src/zeep/wsdl/parse.py
new file mode 100644
index 0000000..865ce15
--- /dev/null
+++ b/src/zeep/wsdl/parse.py
@@ -0,0 +1,164 @@
+from lxml import etree
+
+from zeep.utils import qname_attr
+from zeep.wsdl import definitions
+
+NSMAP = {
+    'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+    'wsaw': 'http://www.w3.org/2006/05/addressing/wsdl',
+}
+
+
+def parse_abstract_message(wsdl, xmlelement):
+    """Create an AbstractMessage object from a xml element.
+
+        <definitions .... >
+            <message name="nmtoken"> *
+                <part name="nmtoken" element="qname"? type="qname"?/> *
+            </message>
+        </definitions>
+    """
+    tns = wsdl.target_namespace
+    parts = []
+
+    for part in xmlelement.findall('wsdl:part', namespaces=NSMAP):
+        part_name = part.get('name')
+        part_element = qname_attr(part, 'element', tns)
+        part_type = qname_attr(part, 'type', tns)
+
+        if part_element is not None:
+            part_element = wsdl.types.get_element(part_element)
+        if part_type is not None:
+            part_type = wsdl.types.get_type(part_type)
+
+        part = definitions.MessagePart(part_element, part_type)
+        parts.append((part_name, part))
+
+    # Create the object, add the parts and return it
+    message_name = qname_attr(xmlelement, 'name', tns)
+    msg = definitions.AbstractMessage(message_name)
+    for part_name, part in parts:
+        msg.add_part(part_name, part)
+    return msg
+
+
+def parse_abstract_operation(wsdl, xmlelement):
+    """Create an AbstractOperation object from a xml element.
+
+    This is called from the parse_port_type function since the abstract
+    operations are part of the port type element.
+
+        <wsdl:operation name="nmtoken">*
+           <wsdl:documentation .... /> ?
+           <wsdl:input name="nmtoken"? message="qname">?
+               <wsdl:documentation .... /> ?
+           </wsdl:input>
+           <wsdl:output name="nmtoken"? message="qname">?
+               <wsdl:documentation .... /> ?
+           </wsdl:output>
+           <wsdl:fault name="nmtoken" message="qname"> *
+               <wsdl:documentation .... /> ?
+           </wsdl:fault>
+        </wsdl:operation>
+
+    """
+    name = xmlelement.get('name')
+    kwargs = {
+        'fault_messages': {}
+    }
+
+    for msg_node in xmlelement.getchildren():
+        tag_name = etree.QName(msg_node.tag).localname
+        if tag_name not in ('input', 'output', 'fault'):
+            continue
+
+        param_msg = qname_attr(
+            msg_node, 'message', wsdl.target_namespace)
+        param_name = msg_node.get('name')
+        param_value = wsdl.get('messages', param_msg.text)
+
+        if tag_name == 'input':
+            kwargs['input_message'] = param_value
+        elif tag_name == 'output':
+            kwargs['output_message'] = param_value
+        else:
+            kwargs['fault_messages'][param_name] = param_value
+
+        wsa_action = msg_node.get(etree.QName(NSMAP['wsaw'], 'Action'))
+        param_value.wsa_action = wsa_action
+
+    kwargs['name'] = name
+    kwargs['parameter_order'] = xmlelement.get('parameterOrder')
+    return definitions.AbstractOperation(**kwargs)
+
+
+def parse_port_type(wsdl, xmlelement):
+    """Create a PortType object from a xml element.
+
+        <wsdl:definitions .... >
+            <wsdl:portType name="nmtoken">
+                <wsdl:operation name="nmtoken" .... /> *
+            </wsdl:portType>
+        </wsdl:definitions>
+
+    """
+    name = qname_attr(xmlelement, 'name', wsdl.target_namespace)
+    operations = {}
+    for elm in xmlelement.findall('wsdl:operation', namespaces=NSMAP):
+        operation = parse_abstract_operation(wsdl, elm)
+        operations[operation.name] = operation
+    return definitions.PortType(name, operations)
+
+
+def parse_port(wsdl, xmlelement):
+    """Create a Port object from a xml element.
+
+    This is called via the parse_service function since ports are part of the
+    service xml elements.
+
+        <wsdl:port name="nmtoken" binding="qname"> *
+           <wsdl:documentation .... /> ?
+           <-- extensibility element -->
+        </wsdl:port>
+
+    """
+    name = xmlelement.get('name')
+    binding_name = qname_attr(xmlelement, 'binding', wsdl.target_namespace)
+    return definitions.Port(name, binding_name=binding_name, xmlelement=xmlelement)
+
+
+def parse_service(wsdl, xmlelement):
+    """
+
+    Syntax::
+
+        <wsdl:service name="nmtoken"> *
+            <wsdl:documentation .... />?
+            <wsdl:port name="nmtoken" binding="qname"> *
+               <wsdl:documentation .... /> ?
+               <-- extensibility element -->
+            </wsdl:port>
+            <-- extensibility element -->
+        </wsdl:service>
+
+    Example::
+
+          <service name="StockQuoteService">
+            <documentation>My first service</documentation>
+            <port name="StockQuotePort" binding="tns:StockQuoteBinding">
+              <soap:address location="http://example.com/stockquote"/>
+            </port>
+          </service>
+
+    """
+    name = xmlelement.get('name')
+    ports = []
+    for port_node in xmlelement.findall('wsdl:port', namespaces=NSMAP):
+        port = parse_port(wsdl, port_node)
+        if port:
+            ports.append(port)
+
+    obj = definitions.Service(name)
+    for port in ports:
+        obj.add_port(port)
+    return obj
diff --git a/src/zeep/wsdl/utils.py b/src/zeep/wsdl/utils.py
new file mode 100644
index 0000000..f5af860
--- /dev/null
+++ b/src/zeep/wsdl/utils.py
@@ -0,0 +1,19 @@
+from lxml import etree
+
+
+def get_or_create_header(envelope):
+    # find the namespace of the SOAP Envelope (because it's different for SOAP 1.1 and 1.2)
+    root_tag = etree.QName(envelope)
+    soap_envelope_namespace = root_tag.namespace
+    # look for the Header element and create it if not found
+    header_qname = '{%s}Header' % soap_envelope_namespace
+    header = envelope.find(header_qname)
+    if header is None:
+        header = etree.Element(header_qname)
+        envelope.insert(0, header)
+    return header
+
+
+def etree_to_string(node):
+    return etree.tostring(
+        node, pretty_print=True, xml_declaration=True, encoding='utf-8')
diff --git a/src/zeep/wsdl/wsdl.py b/src/zeep/wsdl/wsdl.py
new file mode 100644
index 0000000..3eb247c
--- /dev/null
+++ b/src/zeep/wsdl/wsdl.py
@@ -0,0 +1,387 @@
+from __future__ import print_function
+
+import logging
+import operator
+from collections import OrderedDict
+
+import six
+from lxml import etree
+
+from zeep.parser import absolute_location, load_external, parse_xml
+from zeep.utils import findall_multiple_ns
+from zeep.wsdl import parse
+from zeep.xsd import Schema
+from zeep.xsd.context import ParserContext
+
+NSMAP = {
+    'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+}
+
+logger = logging.getLogger(__name__)
+
+
+class Document(object):
+    """A WSDL Document exists out of one or more definitions.
+
+    There is always one 'root' definition which should be passed as the
+    location to the Document.  This definition can import other definitions.
+    These imports are non-transitive, only the definitions defined in the
+    imported document are available in the parent definition.  This Document is
+    mostly just a simple interface to the root definition.
+
+    After all definitions are loaded the definitions are resolved. This
+    resolves references which were not yet available during the initial
+    parsing phase.
+
+    """
+
+    def __init__(self, location, transport):
+        """Initialize a WSDL document.
+
+        The root definition properties are exposed as entry points.
+
+        :param location: Location of this WSDL
+        :type location: string
+        :param transport: The transport object to be used
+        :type transport: zeep.transports.Transport
+
+        """
+        self.location = location if not hasattr(location, 'read') else None
+        self.transport = transport
+
+        # Dict with all definition objects within this WSDL
+        self._definitions = {}
+        self.types = Schema([], transport=self.transport)
+
+        # Dict with internal schema objects, used for lxml.ImportResolver
+        self._parser_context = ParserContext()
+
+        document = self._load_content(location)
+
+        root_definitions = Definition(self, document, self.location)
+        root_definitions.resolve_imports()
+
+        # Make the wsdl definitions public
+        self.messages = root_definitions.messages
+        self.port_types = root_definitions.port_types
+        self.bindings = root_definitions.bindings
+        self.services = root_definitions.services
+
+    def __repr__(self):
+        return '<WSDL(location=%r)>' % self.location
+
+    def dump(self):
+        namespaces = {v: k for k, v in self.types.prefix_map.items()}
+
+        print('')
+        print("Prefixes:")
+        for prefix, namespace in self.types.prefix_map.items():
+            print(' ' * 4, '%s: %s' % (prefix, namespace))
+
+        print('')
+        print("Global elements:")
+        for elm_obj in sorted(self.types.elements, key=lambda k: six.text_type(k)):
+            value = six.text_type(elm_obj)
+            if hasattr(elm_obj, 'qname') and elm_obj.qname.namespace:
+                value = '%s:%s' % (namespaces[elm_obj.qname.namespace], value)
+            print(' ' * 4, value)
+
+        print('')
+        print("Global types:")
+        for type_obj in sorted(self.types.types, key=lambda k: k.qname or ''):
+            value = six.text_type(type_obj)
+            if getattr(type_obj, 'qname', None) and type_obj.qname.namespace:
+                value = '%s:%s' % (namespaces[type_obj.qname.namespace], value)
+            print(' ' * 4, value)
+
+        print('')
+        print("Bindings:")
+        for binding_obj in sorted(self.bindings.values(), key=lambda k: six.text_type(k)):
+            print(' ' * 4, six.text_type(binding_obj))
+
+        print('')
+        for service in self.services.values():
+            print(six.text_type(service))
+            for port in service.ports.values():
+                print(' ' * 4, six.text_type(port))
+                print(' ' * 8, 'Operations:')
+
+                operations = sorted(
+                    port.binding._operations.values(),
+                    key=operator.attrgetter('name'))
+
+                for operation in operations:
+                    print('%s%s' % (' ' * 12, six.text_type(operation)))
+                print('')
+
+    def _load_content(self, location):
+        """Load the XML content from the given location and return an
+        lxml.Element object.
+
+        :param location: The URL of the document to load
+        :type location: string
+
+        """
+        if hasattr(location, 'read'):
+            return parse_xml(location.read())
+        return load_external(location, self.transport, self.location)
+
+
+class Definition(object):
+    """The Definition represents one wsdl:definition within a Document."""
+
+    def __init__(self, wsdl, doc, location):
+        logger.debug("Creating definition for %s", location)
+        self.wsdl = wsdl
+        self.location = location
+
+        self.types = wsdl.types
+        self.port_types = {}
+        self.messages = {}
+        self.bindings = {}
+        self.services = OrderedDict()
+
+        self.imports = {}
+        self._resolved_imports = False
+
+        self.target_namespace = doc.get('targetNamespace')
+        self.wsdl._definitions[self.target_namespace] = self
+        self.nsmap = doc.nsmap
+
+        # Process the definitions
+        self.parse_imports(doc)
+
+        self.parse_types(doc)
+        self.messages = self.parse_messages(doc)
+        self.port_types = self.parse_ports(doc)
+        self.bindings = self.parse_binding(doc)
+        self.services = self.parse_service(doc)
+
+    def __repr__(self):
+        return '<Definition(location=%r)>' % self.location
+
+    def get(self, name, key, _processed=None):
+        container = getattr(self, name)
+        if key in container:
+            return container[key]
+
+        # Turns out that no one knows if the wsdl import statement is
+        # transitive or not. WSDL/SOAP specs are awesome... So lets just do it.
+        # TODO: refactor me into something more sane
+        _processed = _processed or set()
+        if self.target_namespace not in _processed:
+            _processed.add(self.target_namespace)
+            for definition in self.imports.values():
+                try:
+                    return definition.get(name, key, _processed)
+                except IndexError:
+                    pass
+        raise IndexError("No definition %r in %r found" % (key, name))
+
+    def resolve_imports(self):
+        """Resolve all root elements (types, messages, etc)."""
+
+        # Simple guard to protect against cyclic imports
+        if self._resolved_imports:
+            return
+        self._resolved_imports = True
+
+        for definition in self.imports.values():
+            definition.resolve_imports()
+
+        for message in self.messages.values():
+            message.resolve(self)
+
+        for port_type in self.port_types.values():
+            port_type.resolve(self)
+
+        for binding in self.bindings.values():
+            binding.resolve(self)
+
+        for service in self.services.values():
+            service.resolve(self)
+
+    def parse_imports(self, doc):
+        """Import other WSDL definitions in this document.
+
+        Note that imports are non-transitive, so only import definitions
+        which are defined in the imported document and ignore definitions
+        imported in that document.
+
+        This should handle recursive imports though:
+
+            A -> B -> A
+            A -> B -> C -> A
+
+        :param doc: The source document
+        :type doc: lxml.etree._Element
+
+        """
+        for import_node in doc.findall("wsdl:import", namespaces=NSMAP):
+            location = import_node.get('location')
+            namespace = import_node.get('namespace')
+            if namespace in self.wsdl._definitions:
+                self.imports[namespace] = self.wsdl._definitions[namespace]
+            else:
+                document = self.wsdl._load_content(location)
+                location = absolute_location(location, self.location)
+                if etree.QName(document.tag).localname == 'schema':
+                    self.types.add_documents([document], location)
+                else:
+                    wsdl = Definition(self.wsdl, document, location)
+                    self.imports[namespace] = wsdl
+
+    def parse_types(self, doc):
+        """Return an xsd.Schema() instance for the given wsdl:types element.
+
+        If the wsdl:types contain multiple schema definitions then a new
+        wrapping xsd.Schema is defined with xsd:import statements linking them
+        together.
+
+        If the wsdl:types doesn't container an xml schema then an empty schema
+        is returned instead.
+
+            <definitions .... >
+                <types>
+                    <xsd:schema .... />*
+                </types>
+            </definitions>
+
+        :param doc: The source document
+        :type doc: lxml.etree._Element
+
+        """
+        namespace_sets = [
+            {
+                'xsd': 'http://www.w3.org/2001/XMLSchema',
+                'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+            },
+            {
+                'xsd': 'http://www.w3.org/1999/XMLSchema',
+                'wsdl': 'http://schemas.xmlsoap.org/wsdl/',
+            },
+        ]
+
+        # Find xsd:schema elements (wsdl:types/xsd:schema)
+        schema_nodes = findall_multiple_ns(
+            doc, 'wsdl:types/xsd:schema', namespace_sets)
+        self.types.add_documents(schema_nodes, self.location)
+
+    def parse_messages(self, doc):
+        """
+            <definitions .... >
+                <message name="nmtoken"> *
+                    <part name="nmtoken" element="qname"? type="qname"?/> *
+                </message>
+            </definitions>
+
+        :param doc: The source document
+        :type doc: lxml.etree._Element
+
+        """
+        result = {}
+        for msg_node in doc.findall("wsdl:message", namespaces=NSMAP):
+            msg = parse.parse_abstract_message(self, msg_node)
+            result[msg.name.text] = msg
+            logger.debug("Adding message: %s", msg.name.text)
+        return result
+
+    def parse_ports(self, doc):
+        """Return dict with `PortType` instances as values
+
+            <wsdl:definitions .... >
+                <wsdl:portType name="nmtoken">
+                    <wsdl:operation name="nmtoken" .... /> *
+                </wsdl:portType>
+            </wsdl:definitions>
+
+        :param doc: The source document
+        :type doc: lxml.etree._Element
+
+        """
+        result = {}
+        for port_node in doc.findall('wsdl:portType', namespaces=NSMAP):
+            port_type = parse.parse_port_type(self, port_node)
+            result[port_type.name.text] = port_type
+            logger.debug("Adding port: %s", port_type.name.text)
+        return result
+
+    def parse_binding(self, doc):
+        """Parse the binding elements and return a dict of bindings.
+
+        Currently supported bindings are Soap 1.1, Soap 1.2., HTTP Get and
+        HTTP Post. The detection of the type of bindings is done by the
+        bindings themselves using the introspection of the xml nodes.
+
+        XML Structure::
+
+            <wsdl:definitions .... >
+                <wsdl:binding name="nmtoken" type="qname"> *
+                    <-- extensibility element (1) --> *
+                    <wsdl:operation name="nmtoken"> *
+                       <-- extensibility element (2) --> *
+                       <wsdl:input name="nmtoken"? > ?
+                           <-- extensibility element (3) -->
+                       </wsdl:input>
+                       <wsdl:output name="nmtoken"? > ?
+                           <-- extensibility element (4) --> *
+                       </wsdl:output>
+                       <wsdl:fault name="nmtoken"> *
+                           <-- extensibility element (5) --> *
+                       </wsdl:fault>
+                    </wsdl:operation>
+                </wsdl:binding>
+            </wsdl:definitions>
+
+        :param doc: The source document
+        :type doc: lxml.etree._Element
+
+        """
+        result = {}
+        if not getattr(self.wsdl.transport, 'supports_async', False):
+            from zeep.wsdl import bindings
+            binding_classes = [
+                bindings.Soap11Binding,
+                bindings.Soap12Binding,
+                bindings.HttpGetBinding,
+                bindings.HttpPostBinding,
+            ]
+        else:
+            from zeep.asyncio import bindings  # Python 3.5+ syntax
+            binding_classes = [
+                bindings.AsyncSoap11Binding,
+                bindings.AsyncSoap12Binding,
+            ]
+
+        for binding_node in doc.findall('wsdl:binding', namespaces=NSMAP):
+            # Detect the binding type
+            binding = None
+            for binding_class in binding_classes:
+                if binding_class.match(binding_node):
+                    binding = binding_class.parse(self, binding_node)
+
+                    logger.debug("Adding binding: %s", binding.name.text)
+                    result[binding.name.text] = binding
+                    break
+        return result
+
+    def parse_service(self, doc):
+        """
+            <wsdl:definitions .... >
+                <wsdl:service .... > *
+                    <wsdl:port name="nmtoken" binding="qname"> *
+                       <-- extensibility element (1) -->
+                    </wsdl:port>
+                </wsdl:service>
+            </wsdl:definitions>
+
+        :param doc: The source document
+        :type doc: lxml.etree._Element
+
+        """
+        result = OrderedDict()
+        for service_node in doc.findall('wsdl:service', namespaces=NSMAP):
+            service = parse.parse_service(self, service_node)
+            result[service.name] = service
+            logger.debug("Adding service: %s", service.name)
+        return result
diff --git a/src/zeep/wsse/__init__.py b/src/zeep/wsse/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/zeep/wsse/username.py b/src/zeep/wsse/username.py
new file mode 100644
index 0000000..5fb0081
--- /dev/null
+++ b/src/zeep/wsse/username.py
@@ -0,0 +1,118 @@
+import base64
+import hashlib
+import os
+
+from lxml.builder import ElementMaker
+
+from zeep.wsse import utils
+
+NSMAP = {
+    'wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
+    'wsu': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
+}
+WSSE = ElementMaker(namespace=NSMAP['wsse'])
+WSU = ElementMaker(namespace=NSMAP['wsu'])
+
+
+class UsernameToken(object):
+    """UsernameToken Profile 1.1
+
+    https://docs.oasis-open.org/wss/v1.1/wss-v1.1-spec-os-UsernameTokenProfile.pdf
+
+    Example response using PasswordText::
+
+        <wsse:Security>
+          <wsse:UsernameToken>
+            <wsse:Username>scott</wsse:Username>
+            <wsse:Password Type="wsse:PasswordText">password</wsse:Password>
+          </wsse:UsernameToken>
+        </wsse:Security>
+
+    Example using PasswordDigest::
+
+        <wsse:Security>
+          <wsse:UsernameToken>
+            <wsse:Username>NNK</wsse:Username>
+            <wsse:Password Type="wsse:PasswordDigest">
+                weYI3nXd8LjMNVksCKFV8t3rgHh3Rw==
+            </wsse:Password>
+            <wsse:Nonce>WScqanjCEAC4mQoBE07sAQ==</wsse:Nonce>
+            <wsu:Created>2003-07-16T01:24:32Z</wsu:Created>
+          </wsse:UsernameToken>
+        </wsse:Security>
+
+    """
+    username_token_profile_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0'  # noqa
+    soap_message_secutity_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0'    # noqa
+
+    def __init__(self, username, password=None, password_digest=None,
+                 use_digest=False, nonce=None, created=None):
+        self.username = username
+        self.password = password
+        self.password_digest = password_digest
+        self.nonce = nonce
+        self.created = created
+        self.use_digest = use_digest
+
+    def sign(self, envelope, headers):
+        security = utils.get_security_header(envelope)
+
+        # The token placeholder might already exists since it is specified in
+        # the WSDL.
+        token = security.find('{%s}UsernameToken' % NSMAP['wsse'])
+        if token is None:
+            token = WSSE.UsernameToken()
+            security.append(token)
+
+        # Create the sub elements of the UsernameToken element
+        elements = [
+            WSSE.Username(self.username)
+        ]
+        if self.password is not None or self.password_digest is not None:
+            if self.use_digest:
+                elements.extend(self._create_password_digest())
+            else:
+                elements.extend(self._create_password_text())
+
+        token.extend(elements)
+        return envelope, headers
+
+    def verify(self, envelope):
+        pass
+
+    def _create_password_text(self):
+        return [
+            WSSE.Password(
+                self.password,
+                Type='%s#PasswordText' % self.username_token_profile_ns)
+        ]
+
+    def _create_password_digest(self):
+        if self.nonce:
+            nonce = self.nonce.encode('utf-8')
+        else:
+            nonce = os.urandom(16)
+        timestamp = utils.get_timestamp(self.created)
+
+        # digest = Base64 ( SHA-1 ( nonce + created + password ) )
+        if not self.password_digest:
+            digest = base64.b64encode(
+                hashlib.sha1(
+                    nonce + timestamp.encode('utf-8') +
+                    self.password.encode('utf-8')
+                ).digest()
+            ).decode('ascii')
+        else:
+            digest = self.password_digest
+
+        return [
+            WSSE.Password(
+                digest,
+                Type='%s#PasswordDigest' % self.username_token_profile_ns
+            ),
+            WSSE.Nonce(
+                base64.b64encode(nonce).decode('utf-8'),
+                EncodingType='%s#Base64Binary' % self.soap_message_secutity_ns
+            ),
+            WSU.Created(timestamp)
+        ]
diff --git a/src/zeep/wsse/utils.py b/src/zeep/wsse/utils.py
new file mode 100644
index 0000000..e7ed5e2
--- /dev/null
+++ b/src/zeep/wsse/utils.py
@@ -0,0 +1,30 @@
+import datetime
+
+import pytz
+from lxml.builder import ElementMaker
+
+from zeep.wsdl.utils import get_or_create_header
+
+NSMAP = {
+    'wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
+}
+WSSE = ElementMaker(namespace=NSMAP['wsse'])
+
+
+def get_security_header(doc):
+    """Return the security header. If the header doesn't exist it will be
+    created.
+
+    """
+    header = get_or_create_header(doc)
+    security = header.find('wsse:Security', namespaces=NSMAP)
+    if security is None:
+        security = WSSE.Security()
+        header.append(security)
+    return security
+
+
+def get_timestamp(timestamp=None):
+    timestamp = timestamp or datetime.datetime.utcnow()
+    timestamp = timestamp.replace(tzinfo=pytz.utc, microsecond=0)
+    return timestamp.isoformat()
diff --git a/src/zeep/xsd/__init__.py b/src/zeep/xsd/__init__.py
new file mode 100644
index 0000000..65cce78
--- /dev/null
+++ b/src/zeep/xsd/__init__.py
@@ -0,0 +1,6 @@
+from zeep.xsd.builtins import *  # noqa
+from zeep.xsd.elements import *  # noqa
+from zeep.xsd.types import *  # noqa
+from zeep.xsd.valueobjects import *  # noqa
+from zeep.xsd.schema import Schema  # noqa
+from zeep.xsd.indicators import *  # noqa
diff --git a/src/zeep/xsd/builtins.py b/src/zeep/xsd/builtins.py
new file mode 100644
index 0000000..5219ae5
--- /dev/null
+++ b/src/zeep/xsd/builtins.py
@@ -0,0 +1,676 @@
+"""
+    Primitive datatypes
+     - string
+     - boolean
+     - decimal
+     - float
+     - double
+     - duration
+     - dateTime
+     - time
+     - date
+     - gYearMonth
+     - gYear
+     - gMonthDay
+     - gDay
+     - gMonth
+     - hexBinary
+     - base64Binary
+     - anyURI
+     - QName
+     - NOTATION
+
+    Derived datatypes
+     - normalizedString
+     - token
+     - language
+     - NMTOKEN
+     - NMTOKENS
+     - Name
+     - NCName
+     - ID
+     - IDREF
+     - IDREFS
+     - ENTITY
+     - ENTITIES
+     - integer
+     - nonPositiveInteger
+     - negativeInteger
+     - long
+     - int
+     - short
+     - byte
+     - nonNegativeInteger
+     - unsignedLong
+     - unsignedInt
+     - unsignedShort
+     - unsignedByte
+     - positiveInteger
+
+
+"""
+from __future__ import division
+
+import base64
+import datetime
+import math
+import re
+from decimal import Decimal as _Decimal
+
+import isodate
+import pytz
+import six
+from lxml import etree
+
+from zeep.utils import qname_attr
+from zeep.xsd.const import xsd_ns, xsi_ns, NS_XSD
+from zeep.xsd.elements import Base
+from zeep.xsd.types import SimpleType
+from zeep.xsd.valueobjects import AnyObject
+
+
+class ParseError(ValueError):
+    pass
+
+
+def check_no_collection(func):
+    def _wrapper(self, value):
+        if isinstance(value, (list, dict, set)):
+            raise ValueError(
+                "The %s type doesn't accept collections as value" % (
+                    self.__class__.__name__))
+
+        return func(self, value)
+    return _wrapper
+
+
+class _BuiltinType(SimpleType):
+    def __init__(self, qname=None, is_global=False):
+        super(_BuiltinType, self).__init__(
+            qname or etree.QName(self._default_qname), is_global)
+
+    def signature(self, depth=()):
+        if self.qname.namespace == NS_XSD:
+            return 'xsd:%s' % self.name
+        return self.name
+
+##
+# Primitive types
+
+
+class String(_BuiltinType):
+    _default_qname = xsd_ns('string')
+    accepted_types = six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return six.text_type(value if value is not None else '')
+
+    def pythonvalue(self, value):
+        return value
+
+
+class Boolean(_BuiltinType):
+    _default_qname = xsd_ns('boolean')
+    accepted_types = (bool,)
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return 'true' if value else 'false'
+
+    def pythonvalue(self, value):
+        """Return True if the 'true' or '1'. 'false' and '0' are legal false
+        values, but we consider everything not true as false.
+
+        """
+        return value in ('true', '1')
+
+
+class Decimal(_BuiltinType):
+    _default_qname = xsd_ns('decimal')
+    accepted_types = (_Decimal, float) + six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return str(value)
+
+    def pythonvalue(self, value):
+        return _Decimal(value)
+
+
+class Float(_BuiltinType):
+    _default_qname = xsd_ns('float')
+    accepted_types = (float, _Decimal) + six.string_types
+
+    def xmlvalue(self, value):
+        return str(value).upper()
+
+    def pythonvalue(self, value):
+        return float(value)
+
+
+class Double(_BuiltinType):
+    _default_qname = xsd_ns('double')
+    accepted_types = (_Decimal, float) + six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return str(value)
+
+    def pythonvalue(self, value):
+        return float(value)
+
+
+class Duration(_BuiltinType):
+    _default_qname = xsd_ns('duration')
+    accepted_types = (isodate.duration.Duration,) + six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return isodate.duration_isoformat(value)
+
+    def pythonvalue(self, value):
+        return isodate.parse_duration(value)
+
+
+class DateTime(_BuiltinType):
+    _default_qname = xsd_ns('dateTime')
+    accepted_types = (datetime.datetime,) + six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return isodate.isostrf.strftime(value, '%Y-%m-%dT%H:%M:%S%Z')
+
+    def pythonvalue(self, value):
+        return isodate.parse_datetime(value)
+
+
+class Time(_BuiltinType):
+    _default_qname = xsd_ns('time')
+    accepted_types = (datetime.time,) + six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return isodate.isostrf.strftime(value, '%H:%M:%S%Z')
+
+    def pythonvalue(self, value):
+        return isodate.parse_time(value)
+
+
+class Date(_BuiltinType):
+    _default_qname = xsd_ns('date')
+    accepted_types = (datetime.date,) + six.string_types
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        if isinstance(value, six.string_types):
+            return value
+        return isodate.isostrf.strftime(value, '%Y-%m-%d')
+
+    def pythonvalue(self, value):
+        return isodate.parse_date(value)
+
+
+class gYearMonth(_BuiltinType):
+    """gYearMonth represents a specific gregorian month in a specific gregorian
+    year.
+
+    Lexical representation: CCYY-MM
+
+    """
+    accepted_types = (datetime.date,) + six.string_types
+    _default_qname = xsd_ns('gYearMonth')
+    _pattern = re.compile(
+        r'^(?P<year>-?\d{4,})-(?P<month>\d\d)(?P<timezone>Z|[-+]\d\d:?\d\d)?$')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        year, month, tzinfo = value
+        return '%04d-%02d%s' % (year, month, _unparse_timezone(tzinfo))
+
+    def pythonvalue(self, value):
+        match = self._pattern.match(value)
+        if not match:
+            raise ParseError()
+        group = match.groupdict()
+        return (
+            int(group['year']), int(group['month']),
+            _parse_timezone(group['timezone']))
+
+
+class gYear(_BuiltinType):
+    """gYear represents a gregorian calendar year.
+
+    Lexical representation: CCYY
+
+    """
+    accepted_types = (datetime.date,) + six.string_types
+    _default_qname = xsd_ns('gYear')
+    _pattern = re.compile(r'^(?P<year>-?\d{4,})(?P<timezone>Z|[-+]\d\d:?\d\d)?$')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        year, tzinfo = value
+        return '%04d%s' % (year, _unparse_timezone(tzinfo))
+
+    def pythonvalue(self, value):
+        match = self._pattern.match(value)
+        if not match:
+            raise ParseError()
+        group = match.groupdict()
+        return (int(group['year']), _parse_timezone(group['timezone']))
+
+
+class gMonthDay(_BuiltinType):
+    """gMonthDay is a gregorian date that recurs, specifically a day of the
+    year such as the third of May.
+
+    Lexical representation: --MM-DD
+
+    """
+    accepted_types = (datetime.date, ) + six.string_types
+    _default_qname = xsd_ns('gMonthDay')
+    _pattern = re.compile(
+        r'^--(?P<month>\d\d)-(?P<day>\d\d)(?P<timezone>Z|[-+]\d\d:?\d\d)?$')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        month, day, tzinfo = value
+        return '--%02d-%02d%s' % (month, day, _unparse_timezone(tzinfo))
+
+    def pythonvalue(self, value):
+        match = self._pattern.match(value)
+        if not match:
+            raise ParseError()
+
+        group = match.groupdict()
+        return (
+            int(group['month']), int(group['day']),
+            _parse_timezone(group['timezone']))
+
+
+class gDay(_BuiltinType):
+    """gDay is a gregorian day that recurs, specifically a day of the month
+    such as the 5th of the month
+
+    Lexical representation: ---DD
+
+    """
+    accepted_types = (datetime.date,) + six.string_types
+    _default_qname = xsd_ns('gDay')
+    _pattern = re.compile(r'^---(?P<day>\d\d)(?P<timezone>Z|[-+]\d\d:?\d\d)?$')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        day, tzinfo = value
+        return '---%02d%s' % (day, _unparse_timezone(tzinfo))
+
+    def pythonvalue(self, value):
+        match = self._pattern.match(value)
+        if not match:
+            raise ParseError()
+        group = match.groupdict()
+        return (int(group['day']), _parse_timezone(group['timezone']))
+
+
+class gMonth(_BuiltinType):
+    """gMonth is a gregorian month that recurs every year.
+
+    Lexical representation: --MM
+
+    """
+    accepted_types = (datetime.date,) + six.string_types
+    _default_qname = xsd_ns('gMonth')
+    _pattern = re.compile(r'^--(?P<month>\d\d)(?P<timezone>Z|[-+]\d\d:?\d\d)?$')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        month, tzinfo = value
+        return '--%d%s' % (month, _unparse_timezone(tzinfo))
+
+    def pythonvalue(self, value):
+        match = self._pattern.match(value)
+        if not match:
+            raise ParseError()
+        group = match.groupdict()
+        return (int(group['month']), _parse_timezone(group['timezone']))
+
+
+class HexBinary(_BuiltinType):
+    accepted_types = six.string_types
+    _default_qname = xsd_ns('hexBinary')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return value
+
+    def pythonvalue(self, value):
+        return value
+
+
+class Base64Binary(_BuiltinType):
+    accepted_types = six.string_types
+    _default_qname = xsd_ns('base64Binary')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return base64.b64encode(value)
+
+    def pythonvalue(self, value):
+        return base64.b64decode(value)
+
+
+class AnyURI(_BuiltinType):
+    accepted_types = six.string_types
+    _default_qname = xsd_ns('anyURI')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return value
+
+    def pythonvalue(self, value):
+        return value
+
+
+class QName(_BuiltinType):
+    accepted_types = six.string_types
+    _default_qname = xsd_ns('QName')
+
+    @check_no_collection
+    def xmlvalue(self, value):
+        return value
+
+    def pythonvalue(self, value):
+        return value
+
+
+class Notation(_BuiltinType):
+    accepted_types = six.string_types
+    _default_qname = xsd_ns('NOTATION')
+
+
+##
+# Derived datatypes
+
+class NormalizedString(String):
+    _default_qname = xsd_ns('normalizedString')
+
+
+class Token(NormalizedString):
+    _default_qname = xsd_ns('token')
+
+
+class Language(Token):
+    _default_qname = xsd_ns('language')
+
+
+class NmToken(Token):
+    _default_qname = xsd_ns('NMTOKEN')
+
+
+class NmTokens(NmToken):
+    _default_qname = xsd_ns('NMTOKENS')
+
+
+class Name(Token):
+    _default_qname = xsd_ns('Name')
+
+
+class NCName(Name):
+    _default_qname = xsd_ns('NCName')
+
+
+class ID(NCName):
+    _default_qname = xsd_ns('ID')
+
+
+class IDREF(NCName):
+    _default_qname = xsd_ns('IDREF')
+
+
+class IDREFS(IDREF):
+    _default_qname = xsd_ns('IDREFS')
+
+
+class Entity(NCName):
+    _default_qname = xsd_ns('ENTITY')
+
+
+class Entities(Entity):
+    _default_qname = xsd_ns('ENTITIES')
+
+
+class Integer(Decimal):
+    _default_qname = xsd_ns('integer')
+
+    def xmlvalue(self, value):
+        return str(value)
+
+    def pythonvalue(self, value):
+        return int(value)
+
+
+class NonPositiveInteger(Integer):
+    _default_qname = xsd_ns('nonPositiveInteger')
+
+
+class NegativeInteger(Integer):
+    _default_qname = xsd_ns('negativeInteger')
+
+
+class Long(Integer):
+    _default_qname = xsd_ns('long')
+
+    def pythonvalue(self, value):
+        return long(value) if six.PY2 else int(value)  # noqa
+
+
+class Int(Long):
+    _default_qname = xsd_ns('int')
+
+
+class Short(Int):
+    _default_qname = xsd_ns('short')
+
+
+class Byte(Short):
+    """A signed 8-bit integer"""
+    _default_qname = xsd_ns('byte')
+
+
+class NonNegativeInteger(Integer):
+    _default_qname = xsd_ns('nonNegativeInteger')
+
+
+class UnsignedLong(NonNegativeInteger):
+    _default_qname = xsd_ns('unsignedLong')
+
+
+class UnsignedInt(UnsignedLong):
+    _default_qname = xsd_ns('unsignedInt')
+
+
+class UnsignedShort(UnsignedInt):
+    _default_qname = xsd_ns('unsignedShort')
+
+
+class UnsignedByte(UnsignedShort):
+    _default_qname = xsd_ns('unsignedByte')
+
+
+class PositiveInteger(NonNegativeInteger):
+    _default_qname = xsd_ns('positiveInteger')
+
+
+##
+# Other
+
+class AnyType(_BuiltinType):
+    _default_qname = xsd_ns('anyType')
+
+    def render(self, parent, value):
+        if isinstance(value, AnyObject):
+            value.xsd_type.render(parent, value.value)
+            parent.set(xsi_ns('type'), value.xsd_type.qname)
+        else:
+            parent.text = self.xmlvalue(value)
+
+    def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
+                         context=None):
+        xsi_type = qname_attr(xmlelement, xsi_ns('type'))
+        xsi_nil = xmlelement.get(xsi_ns('nil'))
+
+        # Handle xsi:nil attribute
+        if xsi_nil == "true":
+            return None
+
+        if xsi_type and schema:
+            xsd_type = schema.get_type(xsi_type)
+
+            # If the xsd_type is xsd:anyType then we will recurs so ignore
+            # that.
+            if isinstance(xsd_type, self.__class__):
+                return xmlelement.text or None
+
+            return xsd_type.parse_xmlelement(
+                xmlelement, schema, context=context)
+
+        if xmlelement.text is None:
+            return
+
+        return self.pythonvalue(xmlelement.text)
+
+    def xmlvalue(self, value):
+        return value
+
+    def pythonvalue(self, value, schema=None):
+        return value
+
+
+class AnySimpleType(AnyType):
+    _default_qname = xsd_ns('anySimpleType')
+
+
+def _parse_timezone(val):
+    """Return a pytz.tzinfo object"""
+    if not val:
+        return
+
+    if val == 'Z' or val == '+00:00':
+        return pytz.utc
+
+    negative = val.startswith('-')
+    minutes = int(val[-2:])
+    minutes += int(val[1:3]) * 60
+
+    if negative:
+        minutes = 0 - minutes
+    return pytz.FixedOffset(minutes)
+
+
+def _unparse_timezone(tzinfo):
+    if not tzinfo:
+        return ''
+
+    if tzinfo == pytz.utc:
+        return 'Z'
+
+    hours = math.floor(tzinfo._minutes / 60)
+    minutes = tzinfo._minutes % 60
+
+    if hours > 0:
+        return '+%02d:%02d' % (hours, minutes)
+    return '-%02d:%02d' % (abs(hours), minutes)
+
+
+default_types = {
+    cls._default_qname: cls() for cls in [
+        # Primitive
+        String,
+        Boolean,
+        Decimal,
+        Float,
+        Double,
+        Duration,
+        DateTime,
+        Time,
+        Date,
+        gYearMonth,
+        gYear,
+        gMonthDay,
+        gDay,
+        gMonth,
+        HexBinary,
+        Base64Binary,
+        AnyURI,
+        QName,
+        Notation,
+
+        # Derived
+        NormalizedString,
+        Token,
+        Language,
+        NmToken,
+        NmTokens,
+        Name,
+        NCName,
+        ID,
+        IDREF,
+        IDREFS,
+        Entity,
+        Entities,
+        Integer,
+        NonPositiveInteger,  # noqa
+        NegativeInteger,
+        Long,
+        Int,
+        Short,
+        Byte,
+        NonNegativeInteger,  # noqa
+        UnsignedByte,
+        UnsignedInt,
+        UnsignedLong,
+        UnsignedShort,
+        PositiveInteger,
+
+        # Other
+        AnyType,
+        AnySimpleType,
+    ]
+}
+
+
+class Schema(Base):
+    name = 'schema'
+    attr_name = 'schema'
+    qname = xsd_ns('schema')
+
+    def clone(self, qname, min_occurs=1, max_occurs=1):
+        return self.__class__()
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        if name in available_kwargs:
+            value = kwargs[name]
+            available_kwargs.remove(name)
+            return {name: value}
+        return {}
+
+    def parse(self, xmlelement, schema, context=None):
+        from zeep.xsd.schema import Schema
+        schema = Schema(xmlelement, schema._transport)
+        context.schemas.append(schema)
+        return schema
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        if xmlelements[0].tag == self.qname:
+            xmlelement = xmlelements.popleft()
+            result = self.parse(xmlelement, schema, context=context)
+            return result
+
+    def resolve(self):
+        return self
+
+
+default_elements = {
+    xsd_ns('schema'): Schema(),
+}
diff --git a/src/zeep/xsd/const.py b/src/zeep/xsd/const.py
new file mode 100644
index 0000000..a2b4a36
--- /dev/null
+++ b/src/zeep/xsd/const.py
@@ -0,0 +1,12 @@
+from lxml import etree
+
+NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
+NS_XSD = 'http://www.w3.org/2001/XMLSchema'
+
+
+def xsi_ns(localname):
+    return etree.QName(NS_XSI, localname)
+
+
+def xsd_ns(localname):
+    return etree.QName(NS_XSD, localname)
diff --git a/src/zeep/xsd/context.py b/src/zeep/xsd/context.py
new file mode 100644
index 0000000..744f665
--- /dev/null
+++ b/src/zeep/xsd/context.py
@@ -0,0 +1,49 @@
+class SchemaRepository(object):
+    """Mapping between schema target namespace and schema object"""
+    def __init__(self):
+        self._schemas = {}
+
+    def add(self, schema):
+        self._schemas[schema._target_namespace] = schema
+
+    def get(self, namespace):
+        if namespace in self._schemas:
+            return self._schemas[namespace]
+
+    def __contains__(self, namespace):
+        return namespace in self._schemas
+
+    def __len__(self):
+        return len(self._schemas)
+
+
+class SchemaNodeRepository(object):
+    """Mapping between schema target namespace and lxml node"""
+    def __init__(self):
+        self._nodes = {}
+
+    def add(self, key, value):
+        self._nodes[key] = value
+
+    def get(self, key):
+        return self._nodes[key]
+
+    def __len__(self):
+        return len(self._nodes)
+
+
+class ParserContext(object):
+    """Parser context when parsing wsdl/xsd files"""
+    def __init__(self):
+        self.schema_nodes = SchemaNodeRepository()
+        self.schema_objects = SchemaRepository()
+
+        # Mapping between internal nodes and original location
+        self.schema_locations = {}
+
+
+class XmlParserContext(object):
+    """Parser context when parsing XML elements"""
+
+    def __init__(self):
+        self.schemas = []
diff --git a/src/zeep/xsd/elements.py b/src/zeep/xsd/elements.py
new file mode 100644
index 0000000..e7e3d87
--- /dev/null
+++ b/src/zeep/xsd/elements.py
@@ -0,0 +1,500 @@
+import copy
+import logging
+
+from lxml import etree
+
+from zeep import exceptions
+from zeep.exceptions import UnexpectedElementError
+from zeep.utils import qname_attr
+from zeep.xsd.const import xsi_ns
+from zeep.xsd.context import XmlParserContext
+from zeep.xsd.utils import max_occurs_iter
+from zeep.xsd.valueobjects import AnyObject  # cyclic import / FIXME
+
+logger = logging.getLogger(__name__)
+
+
+class Base(object):
+
+    @property
+    def accepts_multiple(self):
+        return self.max_occurs != 1
+
+    @property
+    def default_value(self):
+        return None
+
+    @property
+    def is_optional(self):
+        return self.min_occurs == 0
+
+    def parse_args(self, args):
+        result = {}
+        if not args:
+            return result, args
+
+        value = args.pop(0)
+        return {self.attr_name: value}, args
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        raise NotImplementedError()
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        """Consume matching xmlelements and call parse() on each of them"""
+        raise NotImplementedError()
+
+    def signature(self, depth=()):
+        return ''
+
+
+class Any(Base):
+    name = None
+
+    def __init__(self, max_occurs=1, min_occurs=1, process_contents='strict',
+                 restrict=None):
+        """
+
+        :param process_contents: Specifies how the XML processor should handle
+                                 validation against the elements specified by
+                                 this any element
+        :type process_contents: str (strict, lax, skip)
+
+        """
+        super(Any, self).__init__()
+        self.max_occurs = max_occurs
+        self.min_occurs = min_occurs
+        self.restrict = restrict
+        self.process_contents = process_contents
+
+        # cyclic import
+        from zeep.xsd.builtins import AnyType
+        self.type = AnyType()
+
+    def __call__(self, any_object):
+        return any_object
+
+    def __repr__(self):
+        return '<%s(name=%r)>' % (self.__class__.__name__, self.name)
+
+    def accept(self, value):
+        return True
+
+    def parse(self, xmlelement, schema, context=None):
+        if self.process_contents == 'skip':
+            return xmlelement
+
+        qname = etree.QName(xmlelement.tag)
+        for context_schema in context.schemas:
+            if qname.namespace in context_schema._schemas:
+                schema = context_schema
+                break
+
+        xsd_type = qname_attr(xmlelement, xsi_ns('type'))
+        if xsd_type is not None:
+            xsd_type = schema.get_type(xsd_type)
+            return xsd_type.parse_xmlelement(xmlelement, schema, context=context)
+
+        try:
+            element = schema.get_element(xmlelement.tag)
+            return element.parse(xmlelement, schema, context=context)
+        except (exceptions.NamespaceError, exceptions.LookupError):
+            return xmlelement
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        if name in available_kwargs:
+            available_kwargs.remove(name)
+            value = kwargs[name]
+            return {name: value}
+        return {}
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        """Consume matching xmlelements and call parse() on each of them"""
+        result = []
+
+        for i in max_occurs_iter(self.max_occurs):
+            if xmlelements:
+                xmlelement = xmlelements.popleft()
+                item = self.parse(xmlelement, schema, context=context)
+                if item is not None:
+                    result.append(item)
+            else:
+                break
+
+        if not self.accepts_multiple:
+            result = result[0] if result else None
+        return result
+
+    def render(self, parent, value):
+        assert parent is not None
+        if self.accepts_multiple and isinstance(value, list):
+            from zeep.xsd import SimpleType
+
+            if isinstance(self.restrict, SimpleType):
+                for val in value:
+                    node = etree.SubElement(parent, 'item')
+                    node.set(xsi_ns('type'), self.restrict.qname)
+                    self._render_value_item(node, val)
+            elif self.restrict:
+                for val in value:
+                    node = etree.SubElement(parent, self.restrict.name)
+                    # node.set(xsi_ns('type'), self.restrict.qname)
+                    self._render_value_item(node, val)
+            else:
+                for val in value:
+                    self._render_value_item(parent, val)
+        else:
+            self._render_value_item(parent, value)
+
+    def _render_value_item(self, parent, value):
+        if not value:
+            return
+
+        # Check if we received a proper value object. If we receive the wrong
+        # type then return a nice error message
+        if self.restrict:
+            expected_types = (etree._Element,) + self.restrict.accepted_types
+        else:
+            expected_types = (etree._Element, AnyObject)
+
+        if not isinstance(value, expected_types):
+            type_names = [
+                '%s.%s' % (t.__module__, t.__name__) for t in expected_types
+            ]
+            err_message = "Any element received object of type %r, expected %s" % (
+                type(value).__name__, ' or '.join(type_names))
+
+            raise TypeError('\n'.join((
+                err_message,
+                "See http://docs.python-zeep.org/en/master/datastructures.html"
+                "#any-objects for more information"
+            )))
+
+        if isinstance(value, etree._Element):
+            parent.append(value)
+
+        elif self.restrict:
+            if isinstance(value, list):
+                for val in value:
+                    self.restrict.render(parent, val)
+            else:
+                self.restrict.render(parent, value)
+        else:
+            if isinstance(value.value, list):
+                for val in value.value:
+                    value.xsd_elm.render(parent, val)
+            else:
+                value.xsd_elm.render(parent, value.value)
+
+    def resolve(self):
+        return self
+
+    def signature(self, depth=()):
+        if self.restrict:
+            base = self.restrict.name
+        else:
+            base = 'ANY'
+
+        if self.accepts_multiple:
+            return '%s[]' % base
+        return base
+
+
+class Element(Base):
+    def __init__(self, name, type_=None, min_occurs=1, max_occurs=1,
+                 nillable=False, default=None, is_global=False, attr_name=None):
+        if name and not isinstance(name, etree.QName):
+            name = etree.QName(name)
+
+        self.name = name.localname if name else None
+        self.qname = name
+        self.type = type_
+        self.min_occurs = min_occurs
+        self.max_occurs = max_occurs
+        self.nillable = nillable
+        self.is_global = is_global
+        self.default = default
+        self.attr_name = attr_name or self.name
+        # assert type_
+
+    def __str__(self):
+        if self.type:
+            return '%s(%s)' % (self.name, self.type.signature())
+        return '%s()' % self.name
+
+    def __call__(self, *args, **kwargs):
+        instance = self.type(*args, **kwargs)
+        if hasattr(instance, '_xsd_type'):
+            instance._xsd_elm = self
+        return instance
+
+    def __repr__(self):
+        return '<%s(name=%r, type=%r)>' % (
+            self.__class__.__name__, self.name, self.type)
+
+    def __eq__(self, other):
+        return (
+            other is not None and
+            self.__class__ == other.__class__ and
+            self.__dict__ == other.__dict__)
+
+    @property
+    def default_value(self):
+        value = [] if self.accepts_multiple else self.default
+        return value
+
+    def clone(self, name=None, min_occurs=1, max_occurs=1):
+        new = copy.copy(self)
+
+        if name:
+            if not isinstance(name, etree.QName):
+                name = etree.QName(name)
+            new.name = name.localname
+            new.qname = name
+            new.attr_name = new.name
+
+        new.min_occurs = min_occurs
+        new.max_occurs = max_occurs
+        return new
+
+    def parse(self, xmlelement, schema, allow_none=False, context=None):
+        """Process the given xmlelement. If it has an xsi:type attribute then
+        use that for further processing. This should only be done for subtypes
+        of the defined type but for now we just accept everything.
+
+        """
+        context = context or XmlParserContext()
+        instance_type = qname_attr(xmlelement, xsi_ns('type'))
+        if instance_type:
+            xsd_type = schema.get_type(instance_type)
+        else:
+            xsd_type = self.type
+        return xsd_type.parse_xmlelement(
+            xmlelement, schema, allow_none=allow_none, context=context)
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        return self.type.parse_kwargs(
+            kwargs, name or self.attr_name, available_kwargs)
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        """Consume matching xmlelements and call parse() on each of them"""
+        result = []
+        num_matches = 0
+        for i in max_occurs_iter(self.max_occurs):
+            if not xmlelements:
+                break
+
+            # Workaround for SOAP servers which incorrectly use unqualified
+            # or qualified elements in the responses (#170, #176). To make the
+            # best of it we compare the full uri's if both elements have a
+            # namespace. If only one has a namespace then only compare the
+            # localname.
+
+            # If both elements have a namespace and they don't match then skip
+            element_tag = etree.QName(xmlelements[0].tag)
+            if (
+                element_tag.namespace and self.qname.namespace and
+                element_tag.namespace != self.qname.namespace
+            ):
+                break
+
+            # Only compare the localname
+            if element_tag.localname == self.qname.localname:
+                xmlelement = xmlelements.popleft()
+                num_matches += 1
+                item = self.parse(
+                    xmlelement, schema, allow_none=True, context=context)
+                if item is not None:
+                    result.append(item)
+            else:
+                # If the element passed doesn't match and the current one is
+                # not optional then throw an error
+                if num_matches == 0 and not self.is_optional:
+                    raise UnexpectedElementError(
+                        "Unexpected element %r, expected %r" % (
+                            element_tag.text, self.qname.text))
+                break
+
+        if not self.accepts_multiple:
+            result = result[0] if result else None
+        return result
+
+    def render(self, parent, value):
+        """Render the value(s) on the parent lxml.Element.
+
+        This actually just calls _render_value_item for each value.
+
+        """
+        assert parent is not None
+
+        if self.accepts_multiple and isinstance(value, list):
+            for val in value:
+                self._render_value_item(parent, val)
+        else:
+            self._render_value_item(parent, value)
+
+    def _render_value_item(self, parent, value):
+        """Render the value on the parent lxml.Element"""
+        if value is None:
+            if self.is_optional:
+                return
+
+            elm = etree.SubElement(parent, self.qname)
+            if self.nillable:
+                elm.set(xsi_ns('nil'), 'true')
+            return
+
+        if self.name is None:
+            return self.type.render(parent, value)
+
+        node = etree.SubElement(parent, self.qname)
+        xsd_type = getattr(value, '_xsd_type', self.type)
+
+        if xsd_type != self.type:
+            return value._xsd_type.render(node, value, xsd_type)
+        return self.type.render(node, value)
+
+    def resolve_type(self):
+        self.type = self.type.resolve()
+
+    def resolve(self):
+        self.resolve_type()
+        return self
+
+    def signature(self, depth=()):
+        if len(depth) > 0 and self.is_global:
+            return self.name + '()'
+
+        value = self.type.signature(depth)
+        if self.accepts_multiple:
+            return '%s[]' % value
+        return value
+
+
+class Attribute(Element):
+    def __init__(self, name, type_=None, required=False, default=None):
+        super(Attribute, self).__init__(name=name, type_=type_, default=default)
+        self.required = required
+        self.array_type = None
+
+    def parse(self, value):
+        try:
+            return self.type.pythonvalue(value)
+        except (TypeError, ValueError):
+            logger.exception("Error during xml -> python translation")
+            return None
+
+    def render(self, parent, value):
+        if value is None and not self.required:
+            return
+
+        value = self.type.xmlvalue(value)
+        parent.set(self.qname, value)
+
+    def clone(self, *args, **kwargs):
+        array_type = kwargs.pop('array_type', None)
+        new = super(Attribute, self).clone(*args, **kwargs)
+        new.array_type = array_type
+        return new
+
+    def resolve(self):
+        retval = super(Attribute, self).resolve()
+        self.type = self.type.resolve()
+        if self.array_type:
+            retval.array_type = self.array_type.resolve()
+        return retval
+
+
+class AttributeGroup(Element):
+    def __init__(self, name, attributes):
+        self.name = name
+        self.type = None
+        self._attributes = attributes
+        super(AttributeGroup, self).__init__(name, is_global=True)
+
+    @property
+    def attributes(self):
+        result = []
+        for attr in self._attributes:
+            if isinstance(attr, AttributeGroup):
+                result.extend(attr.attributes)
+            else:
+                result.append(attr)
+        return result
+
+    def resolve(self):
+        resolved = []
+        for attribute in self._attributes:
+            value = attribute.resolve()
+            assert value is not None
+            if isinstance(value, list):
+                resolved.extend(value)
+            else:
+                resolved.append(value)
+        self._attributes = resolved
+        return self
+
+    def signature(self, depth=()):
+        return ', '.join(attr.signature() for attr in self._attributes)
+
+
+class AnyAttribute(Base):
+    name = None
+
+    def __init__(self, process_contents='strict'):
+        self.qname = None
+        self.process_contents = process_contents
+
+    def parse(self, attributes, context=None):
+        return attributes
+
+    def resolve(self):
+        return self
+
+    def render(self, parent, value):
+        if value is None:
+            return
+
+        for name, val in value.items():
+            parent.set(name, val)
+
+    def signature(self, depth=()):
+        return '{}'
+
+
+class RefElement(object):
+
+    def __init__(self, tag, ref, schema, is_qualified=False,
+                 min_occurs=1, max_occurs=1):
+        self._ref = ref
+        self._is_qualified = is_qualified
+        self._schema = schema
+        self.min_occurs = min_occurs
+        self.max_occurs = max_occurs
+
+    def resolve(self):
+        elm = self._schema.get_element(self._ref)
+        elm = elm.clone(
+            elm.qname, min_occurs=self.min_occurs, max_occurs=self.max_occurs)
+        return elm.resolve()
+
+
+class RefAttribute(RefElement):
+    def __init__(self, *args, **kwargs):
+        self._array_type = kwargs.pop('array_type', None)
+        super(RefAttribute, self).__init__(*args, **kwargs)
+
+    def resolve(self):
+        attrib = self._schema.get_attribute(self._ref)
+        attrib = attrib.clone(attrib.qname, array_type=self._array_type)
+        return attrib.resolve()
+
+
+class RefAttributeGroup(RefElement):
+    def resolve(self):
+        value = self._schema.get_attribute_group(self._ref)
+        return value.resolve()
+
+
+class RefGroup(RefElement):
+    def resolve(self):
+        return self._schema.get_group(self._ref)
diff --git a/src/zeep/xsd/indicators.py b/src/zeep/xsd/indicators.py
new file mode 100644
index 0000000..2ba117c
--- /dev/null
+++ b/src/zeep/xsd/indicators.py
@@ -0,0 +1,565 @@
+from __future__ import print_function
+
+import copy
+import operator
+from collections import OrderedDict, defaultdict, deque
+
+from cached_property import threaded_cached_property
+
+from zeep.exceptions import UnexpectedElementError
+from zeep.xsd.elements import Any, Base, Element
+from zeep.xsd.utils import (
+    NamePrefixGenerator, UniqueNameGenerator, max_occurs_iter)
+
+__all__ = ['All', 'Choice', 'Group', 'Sequence']
+
+
+class Indicator(Base):
+
+    def __repr__(self):
+        return '<%s(%s)>' % (
+            self.__class__.__name__, super(Indicator, self).__repr__())
+
+    @threaded_cached_property
+    def default_value(self):
+        return OrderedDict([
+            (name, element.default_value) for name, element in self.elements
+        ])
+
+    def clone(self, name, min_occurs=1, max_occurs=1):
+        raise NotImplementedError()
+
+
+class OrderIndicator(Indicator, list):
+    name = None
+
+    def __init__(self, elements=None, min_occurs=1, max_occurs=1):
+        self.min_occurs = min_occurs
+        self.max_occurs = max_occurs
+
+        if elements is None:
+            super(OrderIndicator, self).__init__()
+        else:
+            super(OrderIndicator, self).__init__()
+            self.extend(elements)
+
+    def clone(self, name, min_occurs=1, max_occurs=1):
+        return self.__class__(
+            elements=list(self),
+            min_occurs=min_occurs,
+            max_occurs=max_occurs)
+
+    @threaded_cached_property
+    def elements(self):
+        """List of tuples containing the element name and the element"""
+        result = []
+        for name, elm in self.elements_nested:
+            if name is None:
+                result.extend(elm.elements)
+            else:
+                result.append((name, elm))
+        return result
+
+    @threaded_cached_property
+    def elements_nested(self):
+        """List of tuples containing the element name and the element"""
+        result = []
+        generator = NamePrefixGenerator()
+        generator_2 = UniqueNameGenerator()
+
+        for elm in self:
+            if isinstance(elm, (All, Choice, Group, Sequence)):
+                if elm.accepts_multiple:
+                    result.append((generator.get_name(), elm))
+                else:
+                    for sub_name, sub_elm in elm.elements:
+                        sub_name = generator_2.create_name(sub_name)
+                    result.append((None, elm))
+            elif isinstance(elm, (Any, Choice)):
+                result.append((generator.get_name(), elm))
+            else:
+                name = generator_2.create_name(elm.attr_name)
+                result.append((name, elm))
+        return result
+
+    def accept(self, values):
+        """Return the number of values which are accepted by this choice.
+
+        If not all required elements are available then 0 is returned.
+
+        """
+        num = 0
+        for name, element in self.elements_nested:
+            if isinstance(element, Element):
+                if element.name in values and values[element.name] is not None:
+                    num += 1
+            else:
+                num += element.accept(values)
+        return num
+
+    def parse_args(self, args):
+        result = {}
+        for name, element in self.elements:
+            if not args:
+                break
+            arg = args.pop(0)
+            result[name] = arg
+
+        return result, args
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        """Apply the given kwarg to the element.
+
+        The available_kwargs is modified in-place. Returns a dict with the
+        result.
+
+        """
+        if self.accepts_multiple:
+            assert name
+
+        if name and name in available_kwargs:
+
+            # Make sure we have a list, lame lame
+            item_kwargs = kwargs.get(name)
+            if not isinstance(item_kwargs, list):
+                item_kwargs = [item_kwargs]
+
+            result = []
+            for i, item_value in zip(max_occurs_iter(self.max_occurs), item_kwargs):
+                item_kwargs = set(item_value.keys())
+                subresult = OrderedDict()
+                for item_name, element in self.elements:
+                    value = element.parse_kwargs(item_value, item_name, item_kwargs)
+                    if value is not None:
+                        subresult.update(value)
+
+                result.append(subresult)
+
+            if self.accepts_multiple:
+                result = {name: result}
+            else:
+                result = result[0] if result else None
+
+            # All items consumed
+            if not any(filter(None, item_kwargs)):
+                available_kwargs.remove(name)
+
+            return result
+
+        else:
+            result = OrderedDict()
+            for elm_name, element in self.elements_nested:
+                sub_result = element.parse_kwargs(kwargs, elm_name, available_kwargs)
+                if sub_result:
+                    result.update(sub_result)
+
+            if name:
+                result = {name: result}
+
+            return result
+
+    def resolve(self):
+        for i, elm in enumerate(self):
+            self[i] = elm.resolve()
+        return self
+
+    def render(self, parent, value):
+        """Create subelements in the given parent object.
+
+        To make sure we render values only once the value items are copied
+        and the rendered attribute is removed from it once it is rendered.
+
+        """
+        if not isinstance(value, list):
+            values = [value]
+        else:
+            values = value
+
+        for i, value in zip(max_occurs_iter(self.max_occurs), values):
+            for name, element in self.elements_nested:
+                if name:
+                    if name in value:
+                        element_value = value[name]
+                        del value[name]
+                    else:
+                        element_value = None
+                else:
+                    element_value = value
+                if element_value is not None or not element.is_optional:
+                    element.render(parent, element_value)
+
+    def signature(self, depth=()):
+        """
+        Use a tuple of element names as depth indicator, so that when an element is repeated,
+        do not try to create its signature, as it would lead to infinite recursion
+        """
+        depth += (self.name,)
+        parts = []
+        for name, element in self.elements_nested:
+            if hasattr(element, 'type') and element.type.name and element.type.name in depth:
+                parts.append('{}: {}'.format(name, element.type.name))
+            elif name:
+                parts.append('%s: %s' % (name, element.signature(depth)))
+            elif isinstance(element,  Indicator):
+                parts.append('%s' % (element.signature(depth)))
+            else:
+                parts.append('%s: %s' % (name, element.signature(depth)))
+        part = ', '.join(parts)
+
+        if self.accepts_multiple:
+            return '[%s]' % (part,)
+        return part
+
+
+class All(OrderIndicator):
+    """Allows the elements in the group to appear (or not appear) in any order
+    in the containing element.
+
+    """
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        result = OrderedDict()
+        expected_tags = {element.qname for __, element in self.elements}
+        consumed_tags = set()
+
+        values = defaultdict(deque)
+        for i, elm in enumerate(xmlelements):
+            if elm.tag in expected_tags:
+                consumed_tags.add(i)
+                values[elm.tag].append(elm)
+
+        # Remove the consumed tags from the xmlelements
+        for i in sorted(consumed_tags, reverse=True):
+            del xmlelements[i]
+
+        for name, element in self.elements:
+            sub_elements = values.get(element.qname)
+            if sub_elements:
+                result[name] = element.parse_xmlelements(
+                    sub_elements, schema, context=context)
+
+        return result
+
+
+class Choice(OrderIndicator):
+
+    @property
+    def is_optional(self):
+        return True
+
+    @property
+    def default_value(self):
+        return OrderedDict()
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        """Return a dictionary"""
+        result = []
+
+        for i in max_occurs_iter(self.max_occurs):
+            if len(xmlelements) < 1:
+                break
+            for node in list(xmlelements):
+
+                # Choose out of multiple
+                options = []
+                for element_name, element in self.elements_nested:
+
+                    local_xmlelements = copy.copy(xmlelements)
+
+                    try:
+                        sub_result = element.parse_xmlelements(
+                            local_xmlelements, schema, context=context)
+                    except UnexpectedElementError:
+                        continue
+
+                    if isinstance(element, OrderIndicator):
+                        if element.accepts_multiple:
+                            sub_result = {element_name: sub_result}
+                    else:
+                        sub_result = {element_name: sub_result}
+
+                    num_consumed = len(xmlelements) - len(local_xmlelements)
+                    if num_consumed:
+                        options.append((num_consumed, sub_result))
+
+                if not options:
+                    xmlelements = []
+                    break
+
+                # Sort on least left
+                options = sorted(options, key=operator.itemgetter(0), reverse=True)
+                if options:
+                    result.append(options[0][1])
+                    for i in range(options[0][0]):
+                        xmlelements.popleft()
+                else:
+                    break
+
+        if self.accepts_multiple:
+            result = {name: result}
+        else:
+            result = result[0] if result else {}
+        return result
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        """Processes the kwargs for this choice element.
+
+        Returns a dict containing the values found.
+
+        This handles two distinct initialization methods:
+
+        1. Passing the choice elements directly to the kwargs (unnested)
+        2. Passing the choice elements into the `name` kwarg (_alue_1) (nested).
+           This case is required when multiple choice elements are given.
+
+        :param name: Name of the choice element (_value_1)
+        :type name: str
+        :param element: Choice element object
+        :type element: zeep.xsd.Choice
+        :param kwargs: dict (or list of dicts) of kwargs for initialization
+        :type kwargs: list / dict
+
+        """
+        if name and name in available_kwargs:
+            values = kwargs[name] or []
+            available_kwargs.remove(name)
+            result = []
+
+            if isinstance(values, dict):
+                values = [values]
+
+            for value in values:
+                for element in self:
+                    # TODO: Use most greedy choice instead of first matching
+                    if isinstance(element, OrderIndicator):
+                        choice_value = value[name] if name in value else value
+                        if element.accept(choice_value):
+                            result.append(choice_value)
+                            break
+                    else:
+                        if element.name in value:
+                            choice_value = value.get(element.name)
+                            result.append({element.name: choice_value})
+                            break
+                else:
+                    raise TypeError(
+                        "No complete xsd:Sequence found for the xsd:Choice %r.\n"
+                        "The signature is: %s" % (name, self.signature()))
+
+            if not self.accepts_multiple:
+                result = result[0] if result else None
+        else:
+            # Direct use-case isn't supported when maxOccurs > 1
+            if self.accepts_multiple:
+                return {}
+
+            result = {}
+
+            # When choice elements are specified directly in the kwargs
+            found = False
+            for i, choice in enumerate(self):
+                temp_kwargs = copy.copy(available_kwargs)
+                subresult = choice.parse_kwargs(kwargs, None, temp_kwargs)
+
+                if subresult:
+                    if not any(subresult.values()):
+                        available_kwargs.intersection_update(temp_kwargs)
+                        result.update(subresult)
+                    elif not found:
+                        available_kwargs.intersection_update(temp_kwargs)
+                        result.update(subresult)
+                        found = True
+            if found:
+                for choice_name, choice in self.elements:
+                    result.setdefault(choice_name, None)
+            else:
+                result = {}
+
+        if name and self.accepts_multiple:
+            result = {name: result}
+        return result
+
+    def render(self, parent, value):
+        """Render the value to the parent element tree node.
+
+        This is a bit more complex then the order render methods since we need
+        to search for the best matching choice element.
+
+        """
+        if not self.accepts_multiple:
+            value = [value]
+
+        for item in value:
+            result = self._find_element_to_render(item)
+            if result:
+                element, choice_value = result
+                element.render(parent, choice_value)
+
+    def accept(self, values):
+        """Return the number of values which are accepted by this choice.
+
+        If not all required elements are available then 0 is returned.
+
+        """
+        nums = set()
+        for name, element in self.elements_nested:
+            if isinstance(element, Element):
+                if name in values and values[name]:
+                    nums.add(1)
+            else:
+                num = element.accept(values)
+                nums.add(num)
+        return max(nums)
+
+    def _find_element_to_render(self, value):
+        """Return a tuple (element, value) for the best matching choice"""
+        matches = []
+
+        for name, element in self.elements_nested:
+            if isinstance(element, Element):
+                if element.name in value:
+                    try:
+                        choice_value = value[element.name]
+                    except KeyError:
+                        choice_value = value
+
+                    if choice_value is not None:
+                        matches.append((1, element, choice_value))
+            else:
+                if name is not None:
+                    try:
+                        choice_value = value[name]
+                    except KeyError:
+                        choice_value = value
+                else:
+                    choice_value = value
+
+                score = element.accept(choice_value)
+                if score:
+                    matches.append((score, element, choice_value))
+
+        if matches:
+            matches = sorted(matches, key=operator.itemgetter(0), reverse=True)
+            return matches[0][1:]
+
+    def signature(self, depth=()):
+        parts = []
+        for name, element in self.elements_nested:
+            if isinstance(element, OrderIndicator):
+                parts.append('{%s}' % (element.signature(depth)))
+            else:
+                parts.append('{%s: %s}' % (name, element.signature(depth)))
+        part = '(%s)' % ' | '.join(parts)
+        if self.accepts_multiple:
+            return '%s[]' % (part,)
+        return part
+
+
+class Sequence(OrderIndicator):
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        result = []
+        for item in max_occurs_iter(self.max_occurs):
+            item_result = OrderedDict()
+            for elm_name, element in self.elements:
+                item_subresult = element.parse_xmlelements(
+                    xmlelements, schema, name, context=context)
+
+                # Unwrap if allowed
+                if isinstance(element, OrderIndicator):
+                    item_result.update(item_subresult)
+                else:
+                    item_result[elm_name] = item_subresult
+
+                if not xmlelements:
+                    break
+            if item_result:
+                result.append(item_result)
+
+        if not self.accepts_multiple:
+            return result[0] if result else None
+
+        return {name: result}
+
+
+class Group(Indicator):
+    """Groups a set of element declarations so that they can be incorporated as
+    a group into complex type definitions.
+
+    """
+
+    def __init__(self, name, child, max_occurs=1, min_occurs=1):
+        super(Group, self).__init__()
+        self.child = child
+        self.qname = name
+        self.name = name.localname
+        self.max_occurs = max_occurs
+        self.min_occurs = min_occurs
+
+    def clone(self, name, min_occurs=1, max_occurs=1):
+        return self.__class__(
+            name=self.qname,
+            child=self.child,
+            min_occurs=min_occurs,
+            max_occurs=max_occurs)
+
+    def __str__(self):
+        return '%s(%s)' % (self.name, self.signature())
+
+    def __iter__(self, *args, **kwargs):
+        for item in self.child:
+            yield item
+
+    @threaded_cached_property
+    def elements(self):
+        if self.accepts_multiple:
+            return [('_value_1', self.child)]
+        return self.child.elements
+
+    def parse_args(self, args):
+        return self.child.parse_args(args)
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        if self.accepts_multiple:
+            if name not in kwargs:
+                return {}, kwargs
+
+            available_kwargs.remove(name)
+            item_kwargs = kwargs[name]
+
+            result = []
+            sub_name = '_value_1' if self.child.accepts_multiple else None
+            for i, sub_kwargs in zip(max_occurs_iter(self.max_occurs), item_kwargs):
+                available_sub_kwargs = set(sub_kwargs.keys())
+                subresult = self.child.parse_kwargs(
+                    sub_kwargs, sub_name, available_sub_kwargs)
+
+                if subresult:
+                    result.append(subresult)
+            if result:
+                result = {name: result}
+        else:
+            result = self.child.parse_kwargs(kwargs, name, available_kwargs)
+        return result
+
+    def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
+        result = []
+
+        for i in max_occurs_iter(self.max_occurs):
+            result.append(
+                self.child.parse_xmlelements(
+                    xmlelements, schema, name, context=context)
+            )
+        if not self.accepts_multiple and result:
+            return result[0]
+        return {name: result}
+
+    def render(self, *args, **kwargs):
+        return self.child.render(*args, **kwargs)
+
+    def resolve(self):
+        self.child = self.child.resolve()
+        return self
+
+    def signature(self, depth=()):
+        return self.child.signature(depth)
diff --git a/src/zeep/xsd/parser.py b/src/zeep/xsd/parser.py
new file mode 100644
index 0000000..fd674d0
--- /dev/null
+++ b/src/zeep/xsd/parser.py
@@ -0,0 +1,42 @@
+from defusedxml.lxml import fromstring
+from lxml import etree
+
+from six.moves.urllib.parse import urlparse
+from zeep.exceptions import XMLSyntaxError
+from zeep.parser import absolute_location
+
+
+class ImportResolver(etree.Resolver):
+    def __init__(self, transport, parser_context):
+        self.parser_context = parser_context
+        self.transport = transport
+
+    def resolve(self, url, pubid, context):
+        if url.startswith('intschema'):
+            text = etree.tostring(self.parser_context.schema_nodes.get(url))
+            return self.resolve_string(text, context)
+
+        if urlparse(url).scheme in ('http', 'https'):
+            content = self.transport.load(url)
+            return self.resolve_string(content, context)
+
+
+def parse_xml(content, transport, parser_context=None, base_url=None):
+    parser = etree.XMLParser(remove_comments=True)
+    parser.resolvers.add(ImportResolver(transport, parser_context))
+    try:
+        return fromstring(content, parser=parser, base_url=base_url)
+    except etree.XMLSyntaxError as exc:
+        raise XMLSyntaxError("Invalid XML content received (%s)" % exc.message)
+
+
+def load_external(url, transport, parser_context=None, base_url=None):
+    if url.startswith('intschema'):
+        assert parser_context
+        return parser_context.schema_nodes.get(url)
+
+    if base_url:
+        url = absolute_location(url, base_url)
+
+    response = transport.load(url)
+    return parse_xml(response, transport, parser_context, base_url)
diff --git a/src/zeep/xsd/printer.py b/src/zeep/xsd/printer.py
new file mode 100644
index 0000000..3e3f86f
--- /dev/null
+++ b/src/zeep/xsd/printer.py
@@ -0,0 +1,67 @@
+from collections import OrderedDict
+
+from six import StringIO
+
+
+class PrettyPrinter(object):
+    """Cleaner pprint output.
+
+    Heavily inspired by the Python pprint module, but more basic for now.
+
+    """
+    def pformat(self, obj):
+        stream = StringIO()
+        self._format(obj, stream)
+        return stream.getvalue()
+
+    def _format(self, obj, stream, indent=4, level=1):
+        _repr = getattr(type(obj), '__repr__', None)
+        write = stream.write
+
+        if (
+            (isinstance(obj, dict) and _repr is dict.__repr__) or
+            (isinstance(obj, OrderedDict) and _repr == OrderedDict.__repr__)
+        ):
+            write('{\n')
+            num = len(obj)
+
+            if num > 0:
+                for i, (key, value) in enumerate(obj.items()):
+                    write(' ' * (indent * level))
+                    write("'%s'" % key)
+                    write(': ')
+                    self._format(value, stream, level=level + 1)
+                    if i < num - 1:
+                        write(',')
+                    write('\n')
+
+                write(' ' * (indent * (level - 1)))
+            write('}')
+
+        elif isinstance(obj, list) and _repr is list.__repr__:
+            write('[')
+            num = len(obj)
+
+            if num > 0:
+                write('\n')
+                for i, value in enumerate(obj):
+                    write(' ' * (indent * level))
+                    self._format(value, stream, level=level + 1)
+                    if i < num - 1:
+                        write(',')
+                    write('\n')
+                write(' ' * (indent * (level - 1)))
+            write(']')
+        else:
+            value = repr(obj)
+            if '\n' in value:
+                lines = value.split('\n')
+                num = len(lines)
+                for i, line in enumerate(lines):
+                    if i > 0:
+                        write(' ' * (indent * (level - 1)))
+                    write(line)
+                    if i < num - 1:
+                        write('\n')
+            else:
+                write(value)
diff --git a/src/zeep/xsd/schema.py b/src/zeep/xsd/schema.py
new file mode 100644
index 0000000..000ac95
--- /dev/null
+++ b/src/zeep/xsd/schema.py
@@ -0,0 +1,383 @@
+import logging
+from collections import OrderedDict
+
+from lxml import etree
+
+from zeep import exceptions
+from zeep.xsd import builtins as xsd_builtins
+from zeep.xsd import const
+from zeep.xsd.context import ParserContext
+from zeep.xsd.visitor import SchemaVisitor
+
+logger = logging.getLogger(__name__)
+
+
+class Schema(object):
+    """A schema is a collection of schema documents."""
+
+    def __init__(self, node=None, transport=None, location=None,
+                 parser_context=None):
+        self._parser_context = parser_context or ParserContext()
+        self._transport = transport
+
+        self._schemas = OrderedDict()
+        self._prefix_map_auto = {}
+        self._prefix_map_custom = {}
+
+        if not isinstance(node, list):
+            nodes = [node] if node is not None else []
+        else:
+            nodes = node
+        self.add_documents(nodes, location)
+
+    def add_documents(self, schema_nodes, location):
+        documents = []
+        for node in schema_nodes:
+            document = SchemaDocument(
+                node, self._transport, self, location,
+                self._parser_context, location)
+            documents.append(document)
+
+        for document in documents:
+            document.resolve()
+
+        self._prefix_map_auto = self._create_prefix_map()
+
+    def __repr__(self):
+        if self._schemas:
+            main_doc = next(iter(self._schemas.values()))
+            location = main_doc._location
+        else:
+            location = '<none>'
+        return '<Schema(location=%r)>' % location
+
+    @property
+    def prefix_map(self):
+        retval = {}
+        retval.update(self._prefix_map_custom)
+        retval.update({
+            k: v for k, v in self._prefix_map_auto.items()
+            if v not in retval.values()
+        })
+        return retval
+
+    @property
+    def is_empty(self):
+        """Boolean to indicate if this schema contains any types or elements"""
+        return all(schema.is_empty for schema in self._schemas.values())
+
+    @property
+    def namespaces(self):
+        return set(self._schemas.keys())
+
+    @property
+    def elements(self):
+        """Yield all globla xsd.Type objects"""
+        for schema in self._schemas.values():
+            for element in schema._elements.values():
+                yield element
+
+    @property
+    def types(self):
+        """Yield all globla xsd.Type objects"""
+        for schema in self._schemas.values():
+            for type_ in schema._types.values():
+                yield type_
+
+    def get_element(self, qname):
+        """Return a global xsd.Element object with the given qname"""
+        qname = self._create_qname(qname)
+        if qname.text in xsd_builtins.default_elements:
+            return xsd_builtins.default_elements[qname]
+
+        # Handle XSD namespace items
+        if qname.namespace == const.NS_XSD:
+            try:
+                return xsd_builtins.default_elements[qname]
+            except KeyError:
+                raise exceptions.LookupError("No such type %r" % qname.text)
+
+        try:
+            schema = self._get_schema_document(qname.namespace)
+            return schema.get_element(qname)
+        except exceptions.NamespaceError:
+            raise exceptions.NamespaceError((
+                "Unable to resolve element %s. " +
+                "No schema available for the namespace %r."
+            ) % (qname.text, qname.namespace))
+
+    def get_type(self, qname):
+        """Return a global xsd.Type object with the given qname"""
+        qname = self._create_qname(qname)
+
+        # Handle XSD namespace items
+        if qname.namespace == const.NS_XSD:
+            try:
+                return xsd_builtins.default_types[qname]
+            except KeyError:
+                raise exceptions.LookupError("No such type %r" % qname.text)
+
+        try:
+            schema = self._get_schema_document(qname.namespace)
+            return schema.get_type(qname)
+        except exceptions.NamespaceError:
+            raise exceptions.NamespaceError((
+                "Unable to resolve type %s. " +
+                "No schema available for the namespace %r."
+            ) % (qname.text, qname.namespace))
+
+    def get_group(self, qname):
+        """Return a global xsd.Group object with the given qname"""
+        qname = self._create_qname(qname)
+        try:
+            schema = self._get_schema_document(qname.namespace)
+            return schema.get_group(qname)
+        except exceptions.NamespaceError:
+            raise exceptions.NamespaceError((
+                "Unable to resolve group %s. " +
+                "No schema available for the namespace %r."
+            ) % (qname.text, qname.namespace))
+
+    def get_attribute(self, qname):
+        """Return a global xsd.attributeGroup object with the given qname"""
+        qname = self._create_qname(qname)
+        try:
+            schema = self._get_schema_document(qname.namespace)
+            return schema.get_attribute(qname)
+        except exceptions.NamespaceError:
+            raise exceptions.NamespaceError((
+                "Unable to resolve attribute %s. " +
+                "No schema available for the namespace %r."
+            ) % (qname.text, qname.namespace))
+
+    def get_attribute_group(self, qname):
+        """Return a global xsd.attributeGroup object with the given qname"""
+        qname = self._create_qname(qname)
+        try:
+            schema = self._get_schema_document(qname.namespace)
+            return schema.get_attribute_group(qname)
+        except exceptions.NamespaceError:
+            raise exceptions.NamespaceError((
+                "Unable to resolve attributeGroup %s. " +
+                "No schema available for the namespace %r."
+            ) % (qname.text, qname.namespace))
+
+    def merge(self, schema):
+        """Merge an other XSD schema in this one"""
+        for namespace, _schema in schema._schemas.items():
+            self._schemas[namespace] = _schema
+        self._prefix_map_auto = self._create_prefix_map()
+
+    def _create_qname(self, name):
+        """Create an `lxml.etree.QName()` object for the given qname string.
+
+        This also expands the shorthand notation.
+
+        """
+        if isinstance(name, etree.QName):
+            return name
+
+        if not name.startswith('{') and ':' in name and self._prefix_map_auto:
+            prefix, localname = name.split(':', 1)
+            if prefix in self._prefix_map_custom:
+                return etree.QName(self._prefix_map_custom[prefix], localname)
+            elif prefix in self._prefix_map_auto:
+                return etree.QName(self._prefix_map_auto[prefix], localname)
+            else:
+                raise ValueError(
+                    "No namespace defined for the prefix %r" % prefix)
+        else:
+            return etree.QName(name)
+
+    def _create_prefix_map(self):
+        prefix_map = {
+            'xsd': 'http://www.w3.org/2001/XMLSchema',
+        }
+        for i, namespace in enumerate(self._schemas.keys()):
+            if namespace is None:
+                continue
+            prefix_map['ns%d' % i] = namespace
+        return prefix_map
+
+    def set_ns_prefix(self, prefix, namespace):
+        self._prefix_map_custom[prefix] = namespace
+
+    def get_ns_prefix(self, prefix):
+        try:
+            try:
+                return self._prefix_map_custom[prefix]
+            except KeyError:
+                return self._prefix_map_auto[prefix]
+        except KeyError:
+            raise ValueError("No such prefix %r" % prefix)
+
+    def _add_schema_document(self, document):
+        logger.info("Add document with tns %s to schema %s", document._target_namespace, id(self))
+        self._schemas[document._target_namespace] = document
+
+    def _get_schema_document(self, namespace):
+        if namespace not in self._schemas:
+            raise exceptions.NamespaceError(
+                "No schema available for the namespace %r" % namespace)
+        return self._schemas[namespace]
+
+
+class SchemaDocument(object):
+    def __init__(self, node, transport, schema, location, parser_context, base_url):
+        logger.debug("Init schema document for %r", location)
+        assert node is not None
+        assert parser_context
+
+        # Internal
+        self._schema = schema
+        self._base_url = base_url or location
+        self._location = location
+        self._transport = transport
+        self._target_namespace = (
+            node.get('targetNamespace') if node is not None else None)
+        self._elm_instances = []
+
+        self._attribute_groups = {}
+        self._attributes = {}
+        self._elements = {}
+        self._groups = {}
+        self._types = {}
+
+        self._imports = OrderedDict()
+        self._element_form = 'unqualified'
+        self._attribute_form = 'unqualified'
+        self._resolved = False
+        # self._xml_schema = None
+
+        self._schema._add_schema_document(self)
+        parser_context.schema_objects.add(self)
+
+        if node is not None:
+            # Disable XML schema validation for now
+            # if len(node) > 0:
+            #     self.xml_schema = etree.XMLSchema(node)
+
+            visitor = SchemaVisitor(self, parser_context)
+            visitor.visit_schema(node)
+
+    def __repr__(self):
+        return '<SchemaDocument(location=%r, tns=%r, is_empty=%r)>' % (
+            self._location, self._target_namespace, self.is_empty)
+
+    def resolve(self):
+        logger.info("Resolving in schema %s", self)
+
+        if self._resolved:
+            return
+        self._resolved = True
+
+        for schema in self._imports.values():
+            schema.resolve()
+
+        def _resolve_dict(val):
+            for key, obj in val.items():
+                new = obj.resolve()
+                assert new is not None, "resolve() should return an object"
+                val[key] = new
+
+        _resolve_dict(self._attribute_groups)
+        _resolve_dict(self._attributes)
+        _resolve_dict(self._elements)
+        _resolve_dict(self._groups)
+        _resolve_dict(self._types)
+
+        for element in self._elm_instances:
+            element.resolve()
+        self._elm_instances = []
+
+    def register_type(self, name, value):
+        assert not isinstance(value, type)
+        assert value is not None
+
+        if isinstance(name, etree.QName):
+            name = name.text
+        logger.debug("register_type(%r, %r)", name, value)
+        self._types[name] = value
+
+    def register_element(self, name, value):
+        if isinstance(name, etree.QName):
+            name = name.text
+        logger.debug("register_element(%r, %r)", name, value)
+        self._elements[name] = value
+
+    def register_group(self, name, value):
+        if isinstance(name, etree.QName):
+            name = name.text
+        logger.debug("register_group(%r, %r)", name, value)
+        self._groups[name] = value
+
+    def register_attribute(self, name, value):
+        if isinstance(name, etree.QName):
+            name = name.text
+        logger.debug("register_attribute(%r, %r)", name, value)
+        self._attributes[name] = value
+
+    def register_attribute_group(self, name, value):
+        if isinstance(name, etree.QName):
+            name = name.text
+        logger.debug("register_attribute_group(%r, %r)", name, value)
+        self._attribute_groups[name] = value
+
+    def get_type(self, qname):
+        """Return a xsd.Type object from this schema"""
+        try:
+            return self._types[qname]
+        except KeyError:
+            known_items = ', '.join(self._types.keys())
+            raise exceptions.LookupError((
+                "No type '%s' in namespace %s. " +
+                "Available types are: %s"
+            ) % (qname.localname, qname.namespace, known_items or ' - '))
+
+    def get_element(self, qname):
+        """Return a xsd.Element object from this schema"""
+        try:
+            return self._elements[qname]
+        except KeyError:
+            known_items = ', '.join(self._elements.keys())
+            raise exceptions.LookupError((
+                "No element '%s' in namespace %s. " +
+                "Available elements are: %s"
+            ) % (qname.localname, qname.namespace, known_items or ' - '))
+
+    def get_group(self, qname):
+        """Return a xsd.Group object from this schema"""
+        try:
+            return self._groups[qname]
+        except KeyError:
+            known_items = ', '.join(self._groups.keys())
+            raise exceptions.LookupError((
+                "No group '%s' in namespace %s. " +
+                "Available attributes are: %s"
+            ) % (qname.localname, qname.namespace, known_items or ' - '))
+
+    def get_attribute(self, qname):
+        """Return a xsd.Attribute object from this schema"""
+        try:
+            return self._attributes[qname]
+        except KeyError:
+            known_items = ', '.join(self._attributes.keys())
+            raise exceptions.LookupError((
+                "No attribute '%s' in namespace %s. " +
+                "Available attributes are: %s"
+            ) % (qname.localname, qname.namespace, known_items or ' - '))
+
+    def get_attribute_group(self, qname):
+        """Return a xsd.AttributeGroup object from this schema"""
+        try:
+            return self._attribute_groups[qname]
+        except KeyError:
+            known_items = ', '.join(self._attribute_groups.keys())
+            raise exceptions.LookupError((
+                "No attributeGroup '%s' in namespace %s. " +
+                "Available attributeGroups are: %s"
+            ) % (qname.localname, qname.namespace, known_items or ' - '))
+
+    @property
+    def is_empty(self):
+        return not bool(self._imports or self._types or self._elements)
diff --git a/src/zeep/xsd/types.py b/src/zeep/xsd/types.py
new file mode 100644
index 0000000..7ce7522
--- /dev/null
+++ b/src/zeep/xsd/types.py
@@ -0,0 +1,597 @@
+import copy
+import logging
+from collections import OrderedDict, deque
+from itertools import chain
+
+import six
+from cached_property import threaded_cached_property
+
+from zeep.exceptions import XMLParseError, UnexpectedElementError
+from zeep.xsd.const import xsi_ns
+from zeep.xsd.elements import Any, AnyAttribute, AttributeGroup, Element
+from zeep.xsd.indicators import Group, OrderIndicator, Sequence
+from zeep.xsd.utils import NamePrefixGenerator
+from zeep.utils import get_base_class
+from zeep.xsd.valueobjects import CompoundValue
+
+
+logger = logging.getLogger(__name__)
+
+
+class Type(object):
+
+    def __init__(self, qname=None, is_global=False):
+        self.qname = qname
+        self.name = qname.localname if qname else None
+        self._resolved = False
+        self.is_global = is_global
+
+    def accept(self, value):
+        raise NotImplementedError
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        value = None
+        name = name or self.name
+
+        if name in available_kwargs:
+            value = kwargs[name]
+            available_kwargs.remove(name)
+            return {name: value}
+        return {}
+
+    def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
+                         context=None):
+        raise NotImplementedError(
+            '%s.parse_xmlelement() is not implemented' % self.__class__.__name__)
+
+    def parsexml(self, xml, schema=None):
+        raise NotImplementedError
+
+    def render(self, parent, value):
+        raise NotImplementedError(
+            '%s.render() is not implemented' % self.__class__.__name__)
+
+    def resolve(self):
+        raise NotImplementedError(
+            '%s.resolve() is not implemented' % self.__class__.__name__)
+
+    def extend(self, child):
+        raise NotImplementedError(
+            '%s.extend() is not implemented' % self.__class__.__name__)
+
+    def restrict(self, child):
+        raise NotImplementedError(
+            '%s.restrict() is not implemented' % self.__class__.__name__)
+
+    @property
+    def attributes(self):
+        return []
+
+    @classmethod
+    def signature(cls, depth=()):
+        return ''
+
+
+class UnresolvedType(Type):
+    def __init__(self, qname, schema):
+        self.qname = qname
+        assert self.qname.text != 'None'
+        self.schema = schema
+
+    def __repr__(self):
+        return '<%s(qname=%r)>' % (self.__class__.__name__, self.qname)
+
+    def render(self, parent, value):
+        raise RuntimeError(
+            "Unable to render unresolved type %s. This is probably a bug." % (
+                self.qname))
+
+    def resolve(self):
+        retval = self.schema.get_type(self.qname)
+        return retval.resolve()
+
+
+class UnresolvedCustomType(Type):
+
+    def __init__(self, qname, base_type, schema):
+        assert qname is not None
+        self.qname = qname
+        self.name = str(qname.localname)
+        self.schema = schema
+        self.base_type = base_type
+
+    def __repr__(self):
+        return '<%s(qname=%r, base_type=%r)>' % (
+            self.__class__.__name__, self.qname.text, self.base_type)
+
+    def resolve(self):
+        base = self.base_type
+        base = base.resolve()
+
+        cls_attributes = {
+            '__module__': 'zeep.xsd.dynamic_types',
+        }
+
+        if issubclass(base.__class__, UnionType):
+            xsd_type = type(self.name, (base.__class__,), cls_attributes)
+            return xsd_type(base.item_types)
+
+        elif issubclass(base.__class__, SimpleType):
+            xsd_type = type(self.name, (base.__class__,), cls_attributes)
+            return xsd_type(self.qname)
+
+        else:
+            xsd_type = type(self.name, (base.base_class,), cls_attributes)
+            return xsd_type(self.qname)
+
+
+ at six.python_2_unicode_compatible
+class SimpleType(Type):
+    accepted_types = six.string_types
+
+    def __call__(self, *args, **kwargs):
+        """Return the xmlvalue for the given value.
+
+        Expects only one argument 'value'.  The args, kwargs handling is done
+        here manually so that we can return readable error messages instead of
+        only '__call__ takes x arguments'
+
+        """
+        num_args = len(args) + len(kwargs)
+        if num_args != 1:
+            raise TypeError((
+                '%s() takes exactly 1 argument (%d given). ' +
+                'Simple types expect only a single value argument'
+            ) % (self.__class__.__name__, num_args))
+
+        if kwargs and 'value' not in kwargs:
+            raise TypeError((
+                '%s() got an unexpected keyword argument %r. ' +
+                'Simple types expect only a single value argument'
+            ) % (self.__class__.__name__, next(six.iterkeys(kwargs))))
+
+        value = args[0] if args else kwargs['value']
+        return self.xmlvalue(value)
+
+    def __eq__(self, other):
+        return (
+            other is not None and
+            self.__class__ == other.__class__ and
+            self.__dict__ == other.__dict__)
+
+    def __str__(self):
+        return '%s(value)' % (self.__class__.__name__)
+
+    def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
+                         context=None):
+        if xmlelement.text is None:
+            return
+        try:
+            return self.pythonvalue(xmlelement.text)
+        except (TypeError, ValueError):
+            logger.exception("Error during xml -> python translation")
+            return None
+
+    def pythonvalue(self, xmlvalue):
+        raise NotImplementedError(
+            '%s.pytonvalue() not implemented' % self.__class__.__name__)
+
+    def render(self, parent, value):
+        parent.text = self.xmlvalue(value)
+
+    def resolve(self):
+        return self
+
+    def signature(self, depth=()):
+        return self.name
+
+    def xmlvalue(self, value):
+        raise NotImplementedError(
+            '%s.xmlvalue() not implemented' % self.__class__.__name__)
+
+
+class ComplexType(Type):
+    _xsd_name = None
+
+    def __init__(self, element=None, attributes=None,
+                 restriction=None, extension=None, qname=None, is_global=False):
+        if element and type(element) == list:
+            element = Sequence(element)
+
+        self.name = self.__class__.__name__ if qname else None
+        self._element = element
+        self._attributes = attributes or []
+        self._restriction = restriction
+        self._extension = extension
+        super(ComplexType, self).__init__(qname=qname, is_global=is_global)
+
+    def __call__(self, *args, **kwargs):
+        return self._value_class(*args, **kwargs)
+
+    @property
+    def accepted_types(self):
+        return (self._value_class,)
+
+    @threaded_cached_property
+    def _value_class(self):
+        return type(
+            self.__class__.__name__, (CompoundValue,),
+            {'_xsd_type': self, '__module__': 'zeep.objects'})
+
+    def __str__(self):
+        return '%s(%s)' % (self.__class__.__name__, self.signature())
+
+    @threaded_cached_property
+    def attributes(self):
+        generator = NamePrefixGenerator(prefix='_attr_')
+        result = []
+        elm_names = {name for name, elm in self.elements if name is not None}
+        for attr in self._attributes_unwrapped:
+            if attr.name is None:
+                name = generator.get_name()
+            elif attr.name in elm_names:
+                name = 'attr__%s' % attr.name
+            else:
+                name = attr.name
+            result.append((name, attr))
+        return result
+
+    @threaded_cached_property
+    def _attributes_unwrapped(self):
+        attributes = []
+        for attr in self._attributes:
+            if isinstance(attr, AttributeGroup):
+                attributes.extend(attr.attributes)
+            else:
+                attributes.append(attr)
+        return attributes
+
+    @threaded_cached_property
+    def elements(self):
+        """List of tuples containing the element name and the element"""
+        result = []
+        for name, element in self.elements_nested:
+            if isinstance(element, Element):
+                result.append((element.attr_name, element))
+            else:
+                result.extend(element.elements)
+        return result
+
+    @threaded_cached_property
+    def elements_nested(self):
+        """List of tuples containing the element name and the element"""
+        result = []
+        generator = NamePrefixGenerator()
+
+        # Handle wsdl:arrayType objects
+        attrs = {attr.qname.text: attr for attr in self._attributes if attr.qname}
+        array_type = attrs.get('{http://schemas.xmlsoap.org/soap/encoding/}arrayType')
+        if array_type:
+            name = generator.get_name()
+            if isinstance(self._element, Group):
+                return [(name, Sequence([
+                    Any(max_occurs='unbounded', restrict=array_type.array_type)
+                ]))]
+            else:
+                return [(name, self._element)]
+
+        # _element is one of All, Choice, Group, Sequence
+        if self._element:
+            result.append((generator.get_name(), self._element))
+        return result
+
+    def parse_xmlelement(self, xmlelement, schema, allow_none=True,
+                         context=None):
+        """Consume matching xmlelements and call parse() on each"""
+        # If this is an empty complexType (<xsd:complexType name="x"/>)
+        if not self.attributes and not self.elements:
+            return None
+
+        attributes = xmlelement.attrib
+        init_kwargs = OrderedDict()
+
+        # If this complexType extends a simpleType then we have no nested
+        # elements. Parse it directly via the type object. This is the case
+        # for xsd:simpleContent
+        if isinstance(self._element, Element) and isinstance(self._element.type, SimpleType):
+            name, element = self.elements_nested[0]
+            init_kwargs[name] = element.type.parse_xmlelement(
+                xmlelement, schema, name, context=context)
+        else:
+            elements = deque(xmlelement.iterchildren())
+            if allow_none and len(elements) == 0 and len(attributes) == 0:
+                return
+
+            # Parse elements. These are always indicator elements (all, choice,
+            # group, sequence)
+            for name, element in self.elements_nested:
+                try:
+                    result = element.parse_xmlelements(
+                        elements, schema, name, context=context)
+                    if result:
+                        init_kwargs.update(result)
+                except UnexpectedElementError as exc:
+                    raise XMLParseError(exc.message)
+
+            # Check if all children are consumed (parsed)
+            if elements:
+                raise XMLParseError("Unexpected element %r" % elements[0].tag)
+
+        # Parse attributes
+        if attributes:
+            attributes = copy.copy(attributes)
+            for name, attribute in self.attributes:
+                if attribute.name:
+                    if attribute.qname.text in attributes:
+                        value = attributes.pop(attribute.qname.text)
+                        init_kwargs[name] = attribute.parse(value)
+                else:
+                    init_kwargs[name] = attribute.parse(attributes)
+
+        return self(**init_kwargs)
+
+    def render(self, parent, value, xsd_type=None):
+        """Serialize the given value lxml.Element subelements on the parent
+        element.
+
+        """
+        if not self.elements_nested and not self.attributes:
+            return
+
+        # Render attributes
+        for name, attribute in self.attributes:
+            attr_value = getattr(value, name, None)
+            attribute.render(parent, attr_value)
+
+        # Render sub elements
+        for name, element in self.elements_nested:
+            if isinstance(element, Element) or element.accepts_multiple:
+                element_value = getattr(value, name, None)
+            else:
+                element_value = value
+
+            if isinstance(element, Element):
+                element.type.render(parent, element_value)
+            else:
+                element.render(parent, element_value)
+
+        if xsd_type and xsd_type._xsd_name:
+            parent.set(xsi_ns('type'), xsd_type._xsd_name)
+
+    def parse_kwargs(self, kwargs, name, available_kwargs):
+        value = None
+        name = name or self.name
+
+        if name in available_kwargs:
+            value = kwargs[name]
+            available_kwargs.remove(name)
+
+            value = self._create_object(value, name)
+            return {name: value}
+        return {}
+
+    def _create_object(self, value, name):
+        """Return the value as a CompoundValue object"""
+        if value is None:
+            return None
+
+        if isinstance(value, list):
+            return [self._create_object(val, name) for val in value]
+
+        if isinstance(value, CompoundValue):
+            return value
+
+        if isinstance(value, dict):
+            return self(**value)
+
+        # Check if the valueclass only expects one value, in that case
+        # we can try to automatically create an object for it.
+        if len(self.attributes) + len(self.elements) == 1:
+            return self(value)
+
+        raise ValueError((
+            "Error while create XML for complexType '%s': "
+            "Expected instance of type %s, received %r instead."
+        ) % (self.qname or name, self._value_class, type(value)))
+
+    def resolve(self):
+        """Resolve all sub elements and types"""
+        if self._resolved:
+            return self._resolved
+        self._resolved = self
+
+        if self._element:
+            self._element = self._element.resolve()
+
+        resolved = []
+        for attribute in self._attributes:
+            value = attribute.resolve()
+            assert value is not None
+            if isinstance(value, list):
+                resolved.extend(value)
+            else:
+                resolved.append(value)
+        self._attributes = resolved
+
+        if self._extension:
+            self._extension = self._extension.resolve()
+            self._resolved = self.extend(self._extension)
+            return self._resolved
+
+        elif self._restriction:
+            self._restriction = self._restriction.resolve()
+            self._resolved = self.restrict(self._restriction)
+            return self._resolved
+
+        else:
+            return self._resolved
+
+    def extend(self, base):
+        """Create a new complextype instance which is the current type
+        extending the given base type.
+
+        Used for handling xsd:extension tags
+
+        """
+        if isinstance(base, ComplexType):
+            base_attributes = base._attributes_unwrapped
+            base_element = base._element
+        else:
+            base_attributes = []
+            base_element = None
+        attributes = base_attributes + self._attributes_unwrapped
+
+        # Make sure we don't have duplicate (child is leading)
+        if base_attributes and self._attributes_unwrapped:
+            new_attributes = OrderedDict()
+            for attr in attributes:
+                if isinstance(attr, AnyAttribute):
+                    new_attributes['##any'] = attr
+                else:
+                    new_attributes[attr.qname.text] = attr
+            attributes = new_attributes.values()
+
+        # If the base and the current type both have an element defined then
+        # these need to be merged. The base_element might be empty (or just
+        # container a placeholder element).
+        element = []
+        if self._element and base_element:
+            element = self._element.clone(self._element.name)
+            if isinstance(element, OrderIndicator) and isinstance(base_element, OrderIndicator):
+                for item in reversed(base_element):
+                    element.insert(0, item)
+
+            elif isinstance(self._element, Group):
+                raise NotImplementedError('TODO')
+            else:
+                pass  # Element (ignore for now)
+
+        elif self._element or base_element:
+            element = self._element or base_element
+        else:
+            element = Element('_value_1', base)
+
+        new = self.__class__(
+            element=element,
+            attributes=attributes,
+            qname=self.qname)
+        return new
+
+    def restrict(self, base):
+        """Create a new complextype instance which is the current type
+        restricted by the base type.
+
+        Used for handling xsd:restriction
+
+        """
+        attributes = list(
+            chain(base._attributes_unwrapped, self._attributes_unwrapped))
+
+        # Make sure we don't have duplicate (self is leading)
+        if base._attributes_unwrapped and self._attributes_unwrapped:
+            new_attributes = OrderedDict()
+            for attr in attributes:
+                if isinstance(attr, AnyAttribute):
+                    new_attributes['##any'] = attr
+                else:
+                    new_attributes[attr.qname.text] = attr
+            attributes = new_attributes.values()
+
+        new = self.__class__(
+            element=self._element or base._element,
+            attributes=attributes,
+            qname=self.qname)
+        return new.resolve()
+
+    def signature(self, depth=()):
+        if len(depth) > 0 and self.is_global:
+            return self.name
+
+        parts = []
+        depth += (self.name,)
+        for name, element in self.elements_nested:
+            # http://schemas.xmlsoap.org/soap/encoding/ contains cyclic type
+            if isinstance(element, Element) and element.type == self:
+                continue
+
+            part = element.signature(depth)
+            parts.append(part)
+
+        for name, attribute in self.attributes:
+            part = '%s: %s' % (name, attribute.signature(depth))
+            parts.append(part)
+
+        value = ', '.join(parts)
+        if len(depth) > 1:
+            value = '{%s}' % value
+        return value
+
+
+class ListType(SimpleType):
+    """Space separated list of simpleType values"""
+
+    def __init__(self, item_type):
+        self.item_type = item_type
+        super(ListType, self).__init__()
+
+    def __call__(self, value):
+        return value
+
+    def render(self, parent, value):
+        parent.text = self.xmlvalue(value)
+
+    def resolve(self):
+        self.item_type = self.item_type.resolve()
+        self.base_class = self.item_type.__class__
+        return self
+
+    def xmlvalue(self, value):
+        item_type = self.item_type
+        return ' '.join(item_type.xmlvalue(v) for v in value)
+
+    def pythonvalue(self, value):
+        if not value:
+            return []
+        item_type = self.item_type
+        return [item_type.pythonvalue(v) for v in value.split()]
+
+    def signature(self, depth=()):
+        return self.item_type.signature(depth) + '[]'
+
+
+class UnionType(SimpleType):
+
+    def __init__(self, item_types):
+        self.item_types = item_types
+        self.item_class = None
+        assert item_types
+        super(UnionType, self).__init__(None)
+
+    def resolve(self):
+        from zeep.xsd.builtins import _BuiltinType
+
+        self.item_types = [item.resolve() for item in self.item_types]
+        base_class = get_base_class(self.item_types)
+        if issubclass(base_class, _BuiltinType) and base_class != _BuiltinType:
+            self.item_class = base_class
+        return self
+
+    def signature(self, depth=()):
+        return ''
+
+    def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
+                         context=None):
+        if self.item_class:
+            return self.item_class().parse_xmlelement(
+                xmlelement, schema, allow_none, context)
+        return xmlelement.text
+
+    def pythonvalue(self, value):
+        if self.item_class:
+            return self.item_class().pythonvalue(value)
+        return value
+
+    def xmlvalue(self, value):
+        if self.item_class:
+            return self.item_class().xmlvalue(value)
+        return value
diff --git a/src/zeep/xsd/utils.py b/src/zeep/xsd/utils.py
new file mode 100644
index 0000000..108cfe8
--- /dev/null
+++ b/src/zeep/xsd/utils.py
@@ -0,0 +1,33 @@
+from six.moves import range
+
+
+class NamePrefixGenerator(object):
+    def __init__(self, prefix='_value_'):
+        self._num = 1
+        self._prefix = prefix
+
+    def get_name(self):
+        retval = '%s%d' % (self._prefix, self._num)
+        self._num += 1
+        return retval
+
+
+class UniqueNameGenerator(object):
+    def __init__(self):
+        self._unique_count = {}
+
+    def create_name(self, name):
+        if name in self._unique_count:
+            self._unique_count[name] += 1
+            return '%s__%d' % (name, self._unique_count[name])
+        else:
+            self._unique_count[name] = 0
+            return name
+
+
+def max_occurs_iter(max_occurs):
+    assert max_occurs is not None
+    if max_occurs == 'unbounded':
+        return range(0, 2**31-1)
+    else:
+        return range(max_occurs)
diff --git a/src/zeep/xsd/valueobjects.py b/src/zeep/xsd/valueobjects.py
new file mode 100644
index 0000000..2a508df
--- /dev/null
+++ b/src/zeep/xsd/valueobjects.py
@@ -0,0 +1,167 @@
+import copy
+from collections import OrderedDict
+
+import six
+
+from zeep.xsd.printer import PrettyPrinter
+
+__all__ = ['AnyObject', 'CompoundValue']
+
+
+class AnyObject(object):
+    def __init__(self, xsd_object, value):
+        self.xsd_obj = xsd_object
+        self.value = value
+
+    def __repr__(self):
+        return '<%s(type=%r, value=%r)>' % (
+            self.__class__.__name__, self.xsd_elm, self.value)
+
+    def __deepcopy__(self, memo):
+        return type(self)(self.xsd_elm, copy.deepcopy(self.value))
+
+    @property
+    def xsd_type(self):
+        return self.xsd_obj
+
+    @property
+    def xsd_elm(self):
+        return self.xsd_obj
+
+
+class CompoundValue(object):
+
+    def __init__(self, *args, **kwargs):
+        values = OrderedDict()
+
+        # Set default values
+        for container_name, container in self._xsd_type.elements_nested:
+            elm_values = container.default_value
+            if isinstance(elm_values, dict):
+                values.update(elm_values)
+            else:
+                values[container_name] = elm_values
+
+        # Set attributes
+        for attribute_name, attribute in self._xsd_type.attributes:
+            values[attribute_name] = attribute.default_value
+
+        # Set elements
+        items = _process_signature(self._xsd_type, args, kwargs)
+        for key, value in items.items():
+            values[key] = value
+        self.__values__ = values
+
+    def __contains__(self, key):
+        return self.__values__.__contains__(key)
+
+    def __len__(self):
+        return self.__values__.__len__()
+
+    def __iter__(self):
+        return self.__values__.__iter__()
+
+    def __repr__(self):
+        return PrettyPrinter().pformat(self.__values__)
+
+    def __delitem__(self, key):
+        return self.__values__.__delitem__(key)
+
+    def __getitem__(self, key):
+        return self.__values__[key]
+
+    def __setitem__(self, key, value):
+        self.__values__[key] = value
+
+    def __setattr__(self, key, value):
+        if key.startswith('__') or key in ('_xsd_type', '_xsd_elm'):
+            return super(CompoundValue, self).__setattr__(key, value)
+        self.__values__[key] = value
+
+    def __getattribute__(self, key):
+        if key.startswith('__') or key in ('_xsd_type', '_xsd_elm'):
+            return super(CompoundValue, self).__getattribute__(key)
+        try:
+            return self.__values__[key]
+        except KeyError:
+            raise AttributeError(
+                "%s instance has no attribute '%s'" % (
+                    self.__class__.__name__, key))
+
+    def __deepcopy__(self, memo):
+        new = type(self)()
+        new.__values__ = copy.deepcopy(self.__values__)
+        for attr, value in self.__dict__.items():
+            if attr != '__values__':
+                setattr(new, attr, value)
+        return new
+
+
+def _process_signature(xsd_type, args, kwargs):
+    """Return a dict with the args/kwargs mapped to the field name.
+
+    Special handling is done for Choice elements since we need to record which
+    element the user intends to use.
+
+    :param fields: List of tuples (name, element)
+    :type fields: list
+    :param args: arg tuples
+    :type args: tuple
+    :param kwargs: kwargs
+    :type kwargs: dict
+
+
+    """
+    result = OrderedDict()
+    # Process the positional arguments. args is currently still modified
+    # in-place here
+    if args:
+        args = list(args)
+        num_args = len(args)
+
+        for element_name, element in xsd_type.elements_nested:
+            values, args = element.parse_args(args)
+            if not values:
+                break
+            result.update(values)
+
+    if args:
+        for attribute_name, attribute in xsd_type.attributes:
+            result[attribute_name] = args.pop(0)
+
+    if args:
+        raise TypeError(
+            "__init__() takes at most %s positional arguments (%s given)" % (
+                len(result), num_args))
+
+    # Process the named arguments (sequence/group/all/choice). The
+    # available_kwargs set is modified in-place.
+    available_kwargs = set(kwargs.keys())
+    for element_name, element in xsd_type.elements_nested:
+        if element.accepts_multiple:
+            values = element.parse_kwargs(kwargs, element_name, available_kwargs)
+        else:
+            values = element.parse_kwargs(kwargs, None, available_kwargs)
+
+        if values is not None:
+            for key, value in values.items():
+                if key not in result:
+                    result[key] = value
+
+    # Process the named arguments for attributes
+    if available_kwargs:
+        for attribute_name, attribute in xsd_type.attributes:
+            if attribute_name in available_kwargs:
+                available_kwargs.remove(attribute_name)
+                result[attribute_name] = kwargs[attribute_name]
+
+    if available_kwargs:
+        raise TypeError((
+            "%s() got an unexpected keyword argument %r. " +
+            "Signature: (%s)"
+        ) % (
+            xsd_type.qname or 'ComplexType',
+            next(iter(available_kwargs)),
+            xsd_type.signature()))
+
+    return result
diff --git a/src/zeep/xsd/visitor.py b/src/zeep/xsd/visitor.py
new file mode 100644
index 0000000..6a77340
--- /dev/null
+++ b/src/zeep/xsd/visitor.py
@@ -0,0 +1,983 @@
+import keyword
+import logging
+import re
+import warnings
+
+from lxml import etree
+
+from zeep import exceptions
+from zeep.exceptions import XMLParseError, ZeepWarning
+from zeep.parser import absolute_location
+from zeep.utils import as_qname, qname_attr
+from zeep.xsd import builtins as xsd_builtins
+from zeep.xsd import elements as xsd_elements
+from zeep.xsd import indicators as xsd_indicators
+from zeep.xsd import types as xsd_types
+from zeep.xsd.const import xsd_ns
+from zeep.xsd.parser import load_external
+
+logger = logging.getLogger(__name__)
+
+
+class tags(object):
+    pass
+
+
+for name in [
+    'schema', 'import', 'include',
+    'annotation', 'element', 'simpleType', 'complexType',
+    'simpleContent', 'complexContent',
+    'sequence', 'group', 'choice', 'all', 'list', 'union',
+    'attribute', 'any', 'anyAttribute', 'attributeGroup',
+    'restriction', 'extension', 'notation',
+
+]:
+    attr = name if name not in keyword.kwlist else name + '_'
+    setattr(tags, attr, xsd_ns(name))
+
+
+class SchemaVisitor(object):
+    """Visitor which processes XSD files and registers global elements and
+    types in the given schema.
+
+    """
+    def __init__(self, document, parser_context=None):
+        self.document = document
+        self.schema = document._schema
+        self.parser_context = parser_context
+        self._includes = set()
+
+    def process(self, node, parent):
+        visit_func = self.visitors.get(node.tag)
+        if not visit_func:
+            raise ValueError("No visitor defined for %r" % node.tag)
+        result = visit_func(self, node, parent)
+        return result
+
+    def process_ref_attribute(self, node, array_type=None):
+        ref = qname_attr(node, 'ref')
+        if ref:
+            ref = self._create_qname(ref)
+
+            # Some wsdl's reference to xs:schema, we ignore that for now. It
+            # might be better in the future to process the actual schema file
+            # so that it is handled correctly
+            if ref.namespace == 'http://www.w3.org/2001/XMLSchema':
+                return
+            return xsd_elements.RefAttribute(
+                node.tag, ref, self.schema, array_type=array_type)
+
+    def process_reference(self, node, **kwargs):
+        ref = qname_attr(node, 'ref')
+        if not ref:
+            return
+
+        if node.tag == tags.element:
+            cls = xsd_elements.RefElement
+        elif node.tag == tags.attribute:
+            cls = xsd_elements.RefAttribute
+        elif node.tag == tags.group:
+            cls = xsd_elements.RefGroup
+        elif node.tag == tags.attributeGroup:
+            cls = xsd_elements.RefAttributeGroup
+        return cls(node.tag, ref, self.schema, **kwargs)
+
+    def visit_schema(self, node):
+        """
+            <schema
+              attributeFormDefault = (qualified | unqualified): unqualified
+              blockDefault = (#all | List of (extension | restriction | substitution) : ''
+              elementFormDefault = (qualified | unqualified): unqualified
+              finalDefault = (#all | List of (extension | restriction | list | union): ''
+              id = ID
+              targetNamespace = anyURI
+              version = token
+              xml:lang = language
+              {any attributes with non-schema Namespace}...>
+            Content: (
+                (include | import | redefine | annotation)*,
+                (((simpleType | complexType | group | attributeGroup) |
+                  element | attribute | notation),
+                 annotation*)*)
+            </schema>
+
+        """
+        assert node is not None
+
+        self.document._target_namespace = node.get('targetNamespace')
+        self.document._element_form = node.get('elementFormDefault', 'unqualified')
+        self.document._attribute_form = node.get('attributeFormDefault', 'unqualified')
+
+        parent = node
+        for node in node.iterchildren():
+            self.process(node, parent=parent)
+
+    def visit_import(self, node, parent):
+        """
+            <import
+              id = ID
+              namespace = anyURI
+              schemaLocation = anyURI
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?)
+            </import>
+        """
+        schema_node = None
+        namespace = node.get('namespace')
+        location = node.get('schemaLocation')
+        if location:
+            location = absolute_location(location, self.document._base_url)
+
+        if not namespace and not self.document._target_namespace:
+            raise XMLParseError(
+                "The attribute 'namespace' must be existent if the "
+                "importing schema has no target namespace.")
+
+        # Check if the schema is already imported before based on the
+        # namespace. Schema's without namespace are registered as 'None'
+        schema = self.parser_context.schema_objects.get(namespace)
+        if schema:
+            if location and schema._location != location:
+                # Use same warning message as libxml2
+                message = (
+                    "Skipping import of schema located at %r " +
+                    "for the namespace %r, since the namespace was " +
+                    "already imported with the schema located at %r"
+                    ) % (location, namespace or '(null)', schema._location)
+                warnings.warn(message, ZeepWarning, stacklevel=6)
+
+                return
+            logger.debug("Returning existing schema: %r", location)
+            self.document._imports[namespace] = schema
+            return schema
+
+        # Hardcode the mapping between the xml namespace and the xsd for now.
+        # This seems to fix issues with exchange wsdl's, see #220
+        if not location and namespace == 'http://www.w3.org/XML/1998/namespace':
+            location = 'https://www.w3.org/2001/xml.xsd'
+
+        # Silently ignore import statements which we can't resolve via the
+        # namespace and doesn't have a schemaLocation attribute.
+        if not location:
+            logger.debug(
+                "Ignoring import statement for namespace %r " +
+                "(missing schemaLocation)", namespace)
+            return
+
+        # Load the XML
+        schema_node = load_external(
+            location, self.document._transport, self.parser_context)
+
+        # Check if the xsd:import namespace matches the targetNamespace. If
+        # the xsd:import statement didn't specify a namespace then make sure
+        # that the targetNamespace wasn't declared by another schema yet.
+        schema_tns = schema_node.get('targetNamespace')
+        if namespace and schema_tns and namespace != schema_tns:
+            raise XMLParseError((
+                "The namespace defined on the xsd:import doesn't match the "
+                "imported targetNamespace located at %r "
+                ) % (location))
+        elif schema_tns in self.parser_context.schema_objects:
+            schema = self.parser_context.schema_objects.get(schema_tns)
+            message = (
+                "Skipping import of schema located at %r " +
+                "for the namespace %r, since the namespace was " +
+                "already imported with the schema located at %r"
+                ) % (location, namespace or '(null)', schema._location)
+            warnings.warn(message, ZeepWarning, stacklevel=6)
+
+        # If this schema location is 'internal' then retrieve the original
+        # location since that is used as base url for sub include/imports
+        if location in self.parser_context.schema_locations:
+            base_url = self.parser_context.schema_locations[location]
+        else:
+            base_url = location
+
+        schema = self.document.__class__(
+            schema_node, self.document._transport, self.schema, location,
+            self.parser_context, base_url)
+
+        self.document._imports[namespace] = schema
+        return schema
+
+    def visit_include(self, node, parent):
+        """
+        <include
+          id = ID
+          schemaLocation = anyURI
+          {any attributes with non-schema Namespace}...>
+        Content: (annotation?)
+        </include>
+        """
+        if not node.get('schemaLocation'):
+            raise NotImplementedError("schemaLocation is required")
+        location = node.get('schemaLocation')
+
+        if location in self._includes:
+            return
+
+        schema_node = load_external(
+            location, self.document._transport, self.parser_context,
+            base_url=self.document._base_url)
+        self._includes.add(location)
+
+        return self.visit_schema(schema_node)
+
+    def visit_element(self, node, parent):
+        """
+            <element
+              abstract = Boolean : false
+              block = (#all | List of (extension | restriction | substitution))
+              default = string
+              final = (#all | List of (extension | restriction))
+              fixed = string
+              form = (qualified | unqualified)
+              id = ID
+              maxOccurs = (nonNegativeInteger | unbounded) : 1
+              minOccurs = nonNegativeInteger : 1
+              name = NCName
+              nillable = Boolean : false
+              ref = QName
+              substitutionGroup = QName
+              type = QName
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (
+                      (simpleType | complexType)?, (unique | key | keyref)*))
+            </element>
+        """
+        is_global = parent.tag == tags.schema
+
+        # minOccurs / maxOccurs are not allowed on global elements
+        if not is_global:
+            min_occurs, max_occurs = _process_occurs_attrs(node)
+        else:
+            max_occurs = 1
+            min_occurs = 1
+
+        # If the element has a ref attribute then all other attributes cannot
+        # be present. Short circuit that here.
+        # Ref is prohibited on global elements (parent = schema)
+        if not is_global:
+            result = self.process_reference(
+                node, min_occurs=min_occurs, max_occurs=max_occurs)
+            if result:
+                return result
+
+        element_form = node.get('form', self.document._element_form)
+        if element_form == 'qualified' or is_global:
+            qname = qname_attr(node, 'name', self.document._target_namespace)
+        else:
+            qname = etree.QName(node.get('name'))
+
+        children = node.getchildren()
+        xsd_type = None
+        if children:
+            value = None
+
+            for child in children:
+                if child.tag == tags.annotation:
+                    continue
+
+                elif child.tag in (tags.simpleType, tags.complexType):
+                    assert not value
+
+                    xsd_type = self.process(child, node)
+
+        if not xsd_type:
+            node_type = qname_attr(node, 'type')
+            if node_type:
+                xsd_type = self._get_type(node_type.text)
+            else:
+                xsd_type = xsd_builtins.AnyType()
+
+        # Naive workaround to mark fields which are part of a choice element
+        # as optional
+        if parent.tag == tags.choice:
+            min_occurs = 0
+
+        nillable = node.get('nillable') == 'true'
+        default = node.get('default')
+        element = xsd_elements.Element(
+            name=qname, type_=xsd_type,
+            min_occurs=min_occurs, max_occurs=max_occurs, nillable=nillable,
+            default=default, is_global=is_global)
+
+        self.document._elm_instances.append(element)
+
+        # Only register global elements
+        if is_global:
+            self.document.register_element(qname, element)
+        return element
+
+    def visit_attribute(self, node, parent):
+        """Declares an attribute.
+
+            <attribute
+              default = string
+              fixed = string
+              form = (qualified | unqualified)
+              id = ID
+              name = NCName
+              ref = QName
+              type = QName
+              use = (optional | prohibited | required): optional
+              {any attributes with non-schema Namespace...}>
+            Content: (annotation?, (simpleType?))
+            </attribute>
+        """
+        is_global = parent.tag == tags.schema
+
+        # Check of wsdl:arayType
+        array_type = node.get('{http://schemas.xmlsoap.org/wsdl/}arrayType')
+        if array_type:
+            match = re.match('([^\[]+)', array_type)
+            if match:
+                array_type = match.groups()[0]
+                qname = as_qname(
+                    array_type, node.nsmap, self.document._target_namespace)
+                array_type = xsd_types.UnresolvedType(qname, self.schema)
+
+        # If the elment has a ref attribute then all other attributes cannot
+        # be present. Short circuit that here.
+        # Ref is prohibited on global elements (parent = schema)
+        if not is_global:
+            result = self.process_ref_attribute(node, array_type=array_type)
+            if result:
+                return result
+
+        attribute_form = node.get('form', self.document._attribute_form)
+        qname = qname_attr(node, 'name', self.document._target_namespace)
+        if attribute_form == 'qualified' or is_global:
+            name = qname
+        else:
+            name = etree.QName(node.get('name'))
+
+        annotation, items = self._pop_annotation(node.getchildren())
+        if items:
+            xsd_type = self.visit_simple_type(items[0], node)
+        else:
+            node_type = qname_attr(node, 'type')
+            if node_type:
+                xsd_type = self._get_type(node_type)
+            else:
+                xsd_type = xsd_builtins.AnyType()
+
+        # TODO: We ignore 'prohobited' for now
+        required = node.get('use') == 'required'
+        default = node.get('default')
+
+        attr = xsd_elements.Attribute(
+            name, type_=xsd_type, default=default, required=required)
+        self.document._elm_instances.append(attr)
+
+        # Only register global elements
+        if is_global:
+            self.document.register_attribute(qname, attr)
+        return attr
+
+    def visit_simple_type(self, node, parent):
+        """
+            <simpleType
+              final = (#all | (list | union | restriction))
+              id = ID
+              name = NCName
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (restriction | list | union))
+            </simpleType>
+        """
+
+        if parent.tag == tags.schema:
+            name = node.get('name')
+            is_global = True
+        else:
+            name = parent.get('name', 'Anonymous')
+            is_global = False
+        base_type = '{http://www.w3.org/2001/XMLSchema}string'
+        qname = as_qname(name, node.nsmap, self.document._target_namespace)
+
+        annotation, items = self._pop_annotation(node.getchildren())
+        child = items[0]
+        if child.tag == tags.restriction:
+            base_type = self.visit_restriction_simple_type(child, node)
+            xsd_type = xsd_types.UnresolvedCustomType(
+                qname, base_type, self.schema)
+
+        elif child.tag == tags.list:
+            xsd_type = self.visit_list(child, node)
+
+        elif child.tag == tags.union:
+            xsd_type = self.visit_union(child, node)
+        else:
+            raise AssertionError("Unexpected child: %r" % child.tag)
+
+        assert xsd_type is not None
+        if is_global:
+            self.document.register_type(qname, xsd_type)
+        return xsd_type
+
+    def visit_complex_type(self, node, parent):
+        """
+        <complexType
+          abstract = Boolean : false
+          block = (#all | List of (extension | restriction))
+          final = (#all | List of (extension | restriction))
+          id = ID
+          mixed = Boolean : false
+          name = NCName
+          {any attributes with non-schema Namespace...}>
+        Content: (annotation?, (simpleContent | complexContent |
+                  ((group | all | choice | sequence)?,
+                  ((attribute | attributeGroup)*, anyAttribute?))))
+        </complexType>
+
+        """
+        children = []
+        base_type = '{http://www.w3.org/2001/XMLSchema}anyType'
+
+        # If the complexType's parent is an element then this type is
+        # anonymous and should have no name defined. Otherwise it's global
+        if parent.tag == tags.schema:
+            name = node.get('name')
+            is_global = True
+        else:
+            name = parent.get('name')
+            is_global = False
+
+        qname = as_qname(name, node.nsmap, self.document._target_namespace)
+        cls_attributes = {
+            '__module__': 'zeep.xsd.dynamic_types',
+            '_xsd_name': qname,
+        }
+        xsd_cls = type(name, (xsd_types.ComplexType,), cls_attributes)
+        xsd_type = None
+
+        # Process content
+        annotation, children = self._pop_annotation(node.getchildren())
+        first_tag = children[0].tag if children else None
+
+        if first_tag == tags.simpleContent:
+            base_type, attributes = self.visit_simple_content(children[0], node)
+
+            xsd_type = xsd_cls(
+                attributes=attributes, extension=base_type, qname=qname,
+                is_global=is_global)
+
+        elif first_tag == tags.complexContent:
+            kwargs = self.visit_complex_content(children[0], node)
+            xsd_type = xsd_cls(qname=qname, is_global=is_global, **kwargs)
+
+        elif first_tag:
+            element = None
+
+            if first_tag in (tags.group, tags.all, tags.choice, tags.sequence):
+                child = children.pop(0)
+                element = self.process(child, node)
+
+            attributes = self._process_attributes(node, children)
+            xsd_type = xsd_cls(
+                element=element, attributes=attributes, qname=qname,
+                is_global=is_global)
+        else:
+            xsd_type = xsd_cls(qname=qname)
+
+        if is_global:
+            self.document.register_type(qname, xsd_type)
+        return xsd_type
+
+    def visit_complex_content(self, node, parent, namespace=None):
+        """The complexContent element defines extensions or restrictions on a
+        complex type that contains mixed content or elements only.
+
+            <complexContent
+              id = ID
+              mixed = Boolean
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?,  (restriction | extension))
+            </complexContent>
+        """
+
+        child = node.getchildren()[-1]
+
+        if child.tag == tags.restriction:
+            base, element, attributes = self.visit_restriction_complex_content(
+                child, node)
+            return {
+                'attributes': attributes,
+                'element': element,
+                'restriction': base,
+            }
+        elif child.tag == tags.extension:
+            base, element, attributes = self.visit_extension_complex_content(
+                child, node)
+            return {
+                'attributes': attributes,
+                'element': element,
+                'extension': base,
+            }
+
+    def visit_simple_content(self, node, parent, namespace=None):
+        """Contains extensions or restrictions on a complexType element with
+        character data or a simpleType element as content and contains no
+        elements.
+
+            <simpleContent
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (restriction | extension))
+            </simpleContent>
+        """
+
+        child = node.getchildren()[-1]
+
+        if child.tag == tags.restriction:
+            return self.visit_restriction_simple_content(child, node)
+        elif child.tag == tags.extension:
+            return self.visit_extension_simple_content(child, node)
+        raise AssertionError("Expected restriction or extension")
+
+    def visit_restriction_simple_type(self, node, parent, namespace=None):
+        """
+            <restriction
+              base = QName
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?,
+                (simpleType?, (
+                    minExclusive | minInclusive | maxExclusive | maxInclusive |
+                    totalDigits |fractionDigits | length | minLength |
+                    maxLength | enumeration | whiteSpace | pattern)*))
+            </restriction>
+        """
+        base_name = qname_attr(node, 'base')
+        if base_name:
+            return self._get_type(base_name)
+
+        annotation, children = self._pop_annotation(node.getchildren())
+        if children[0].tag == tags.simpleType:
+            return self.visit_simple_type(children[0], node)
+
+    def visit_restriction_simple_content(self, node, parent, namespace=None):
+        """
+            <restriction
+              base = QName
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?,
+                (simpleType?, (
+                    minExclusive | minInclusive | maxExclusive | maxInclusive |
+                    totalDigits |fractionDigits | length | minLength |
+                    maxLength | enumeration | whiteSpace | pattern)*
+                )?, ((attribute | attributeGroup)*, anyAttribute?))
+            </restriction>
+        """
+        base_name = qname_attr(node, 'base')
+        base_type = self._get_type(base_name)
+        return base_type, []
+
+    def visit_restriction_complex_content(self, node, parent, namespace=None):
+        """
+
+            <restriction
+              base = QName
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (group | all | choice | sequence)?,
+                    ((attribute | attributeGroup)*, anyAttribute?))
+            </restriction>
+        """
+        base_name = qname_attr(node, 'base')
+        base_type = self._get_type(base_name)
+        annotation, children = self._pop_annotation(node.getchildren())
+
+        element = None
+        attributes = []
+
+        if children:
+            child = children[0]
+            if child.tag in (tags.group, tags.all, tags.choice, tags.sequence):
+                children.pop(0)
+                element = self.process(child, node)
+            attributes = self._process_attributes(node, children)
+        return base_type, element, attributes
+
+    def visit_extension_complex_content(self, node, parent):
+        """
+            <extension
+              base = QName
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (
+                        (group | all | choice | sequence)?,
+                        ((attribute | attributeGroup)*, anyAttribute?)))
+            </extension>
+        """
+        base_name = qname_attr(node, 'base')
+        base_type = self._get_type(base_name)
+        annotation, children = self._pop_annotation(node.getchildren())
+
+        element = None
+        attributes = []
+
+        if children:
+            child = children[0]
+            if child.tag in (tags.group, tags.all, tags.choice, tags.sequence):
+                children.pop(0)
+                element = self.process(child, node)
+            attributes = self._process_attributes(node, children)
+
+        return base_type, element, attributes
+
+    def visit_extension_simple_content(self, node, parent):
+        """
+            <extension
+              base = QName
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, ((attribute | attributeGroup)*, anyAttribute?))
+            </extension>
+        """
+        base_name = qname_attr(node, 'base')
+        base_type = self._get_type(base_name)
+        annotation, children = self._pop_annotation(node.getchildren())
+        attributes = self._process_attributes(node, children)
+
+        return base_type, attributes
+
+    def visit_annotation(self, node, parent):
+        """Defines an annotation.
+
+            <annotation
+              id = ID
+              {any attributes with non-schema Namespace}...>
+            Content: (appinfo | documentation)*
+            </annotation>
+        """
+        return
+
+    def visit_any(self, node, parent):
+        """
+            <any
+              id = ID
+              maxOccurs = (nonNegativeInteger | unbounded) : 1
+              minOccurs = nonNegativeInteger : 1
+              namespace = "(##any | ##other) |
+                List of (anyURI | (##targetNamespace |  ##local))) : ##any
+              processContents = (lax | skip | strict) : strict
+              {any attributes with non-schema Namespace...}>
+            Content: (annotation?)
+            </any>
+        """
+        min_occurs, max_occurs = _process_occurs_attrs(node)
+        process_contents = node.get('processContents', 'strict')
+        return xsd_elements.Any(
+            max_occurs=max_occurs, min_occurs=min_occurs,
+            process_contents=process_contents)
+
+    def visit_sequence(self, node, parent):
+        """
+            <sequence
+              id = ID
+              maxOccurs = (nonNegativeInteger | unbounded) : 1
+              minOccurs = nonNegativeInteger : 1
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?,
+                      (element | group | choice | sequence | any)*)
+            </sequence>
+        """
+
+        sub_types = [
+            tags.annotation, tags.any, tags.choice, tags.element,
+            tags.group, tags.sequence
+        ]
+        min_occurs, max_occurs = _process_occurs_attrs(node)
+        result = xsd_indicators.Sequence(
+            min_occurs=min_occurs, max_occurs=max_occurs)
+
+        annotation, items = self._pop_annotation(node.getchildren())
+        for child in items:
+            assert child.tag in sub_types, child
+            item = self.process(child, node)
+            assert item is not None
+            result.append(item)
+
+        assert None not in result
+        return result
+
+    def visit_all(self, node, parent):
+        """Allows the elements in the group to appear (or not appear) in any
+        order in the containing element.
+
+            <all
+              id = ID
+              maxOccurs= 1: 1
+              minOccurs= (0 | 1): 1
+              {any attributes with non-schema Namespace...}>
+            Content: (annotation?, element*)
+            </all>
+        """
+
+        sub_types = [
+            tags.annotation, tags.element
+        ]
+        result = xsd_indicators.All()
+
+        for child in node.iterchildren():
+            assert child.tag in sub_types, child
+            item = self.process(child, node)
+            result.append(item)
+
+        assert None not in result
+        return result
+
+    def visit_group(self, node, parent):
+        """Groups a set of element declarations so that they can be
+        incorporated as a group into complex type definitions.
+
+            <group
+              name= NCName
+              id = ID
+              maxOccurs = (nonNegativeInteger | unbounded) : 1
+              minOccurs = nonNegativeInteger : 1
+              name = NCName
+              ref = QName
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (all | choice | sequence))
+            </group>
+
+        """
+
+        result = self.process_reference(node)
+        if result:
+            return result
+
+        qname = qname_attr(node, 'name', self.document._target_namespace)
+
+        # There should be only max nodes, first node (annotation) is irrelevant
+        annotation, children = self._pop_annotation(node.getchildren())
+        child = children[0]
+
+        item = self.process(child, parent)
+        elm = xsd_indicators.Group(name=qname, child=item)
+
+        if parent.tag == tags.schema:
+            self.document.register_group(qname, elm)
+        return elm
+
+    def visit_list(self, node, parent):
+        """
+            <list
+              id = ID
+              itemType = QName
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (simpleType?))
+            </list>
+
+        The use of the simpleType element child and the itemType attribute is
+        mutually exclusive.
+
+        """
+        item_type = qname_attr(node, 'itemType')
+        if item_type:
+            sub_type = self._get_type(item_type.text)
+        else:
+            subnodes = node.getchildren()
+            child = subnodes[-1]  # skip annotation
+            sub_type = self.visit_simple_type(child, node)
+        return xsd_types.ListType(sub_type)
+
+    def visit_choice(self, node, parent):
+        """
+            <choice
+              id = ID
+              maxOccurs= (nonNegativeInteger | unbounded) : 1
+              minOccurs= nonNegativeInteger : 1
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (element | group | choice | sequence | any)*)
+            </choice>
+        """
+        min_occurs, max_occurs = _process_occurs_attrs(node)
+
+        children = node.getchildren()
+        annotation, children = self._pop_annotation(children)
+
+        choices = []
+        for child in children:
+            elm = self.process(child, node)
+            choices.append(elm)
+        return xsd_indicators.Choice(
+            choices, min_occurs=min_occurs, max_occurs=max_occurs)
+
+    def visit_union(self, node, parent):
+        """Defines a collection of multiple simpleType definitions.
+
+            <union
+              id = ID
+              memberTypes = List of QNames
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (simpleType*))
+            </union>
+        """
+        # TODO
+        members = node.get('memberTypes')
+        types = []
+        if members:
+            for member in members.split():
+                qname = as_qname(member, node.nsmap, self.document._target_namespace)
+                xsd_type = self._get_type(qname)
+                types.append(xsd_type)
+        else:
+            annotation, types = self._pop_annotation(node.getchildren())
+            types = [self.visit_simple_type(t, node) for t in types]
+        return xsd_types.UnionType(types)
+
+    def visit_unique(self, node, parent):
+        """Specifies that an attribute or element value (or a combination of
+        attribute or element values) must be unique within the specified scope.
+        The value must be unique or nil.
+
+            <unique
+              id = ID
+              name = NCName
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?, (selector, field+))
+            </unique>
+        """
+        # TODO
+        pass
+
+    def visit_attribute_group(self, node, parent):
+        """
+            <attributeGroup
+              id = ID
+              name = NCName
+              ref = QName
+              {any attributes with non-schema Namespace...}>
+            Content: (annotation?),
+                     ((attribute | attributeGroup)*, anyAttribute?))
+            </attributeGroup>
+        """
+        ref = self.process_reference(node)
+        if ref:
+            return ref
+
+        qname = qname_attr(node, 'name', self.document._target_namespace)
+        annotation, children = self._pop_annotation(node.getchildren())
+
+        attributes = self._process_attributes(node, children)
+        attribute_group = xsd_elements.AttributeGroup(qname, attributes)
+        self.document.register_attribute_group(qname, attribute_group)
+
+    def visit_any_attribute(self, node, parent):
+        """
+            <anyAttribute
+              id = ID
+              namespace = ((##any | ##other) |
+                List of (anyURI | (##targetNamespace | ##local))) : ##any
+              processContents = (lax | skip | strict): strict
+              {any attributes with non-schema Namespace...}>
+            Content: (annotation?)
+            </anyAttribute>
+        """
+        process_contents = node.get('processContents', 'strict')
+        return xsd_elements.AnyAttribute(process_contents=process_contents)
+
+    def visit_notation(self, node, parent):
+        """Contains the definition of a notation to describe the format of
+        non-XML data within an XML document. An XML Schema notation declaration
+        is a reconstruction of XML 1.0 NOTATION declarations.
+
+            <notation
+              id = ID
+              name = NCName
+              public = Public identifier per ISO 8879
+              system = anyURI
+              {any attributes with non-schema Namespace}...>
+            Content: (annotation?)
+            </notation>
+
+        """
+        pass
+
+    def _get_type(self, name):
+        assert name is not None
+        name = self._create_qname(name)
+        try:
+            retval = self.schema.get_type(name)
+        except (exceptions.NamespaceError, exceptions.LookupError):
+            retval = xsd_types.UnresolvedType(name, self.schema)
+        return retval
+
+    def _create_qname(self, name):
+        if not isinstance(name, etree.QName):
+            name = etree.QName(name)
+
+        # Handle reserved namespace
+        if name.namespace == 'xml':
+            name = etree.QName(
+                'http://www.w3.org/XML/1998/namespace', name.localname)
+
+        # Various xsd builders assume that some schema's are available by
+        # default (actually this is mostly just the soap-enc ns). So live with
+        # that fact and handle it by auto-importing the schema if it is
+        # referenced.
+        if (
+            name.namespace == 'http://schemas.xmlsoap.org/soap/encoding/' and
+            name.namespace not in self.document._imports
+        ):
+            import_node = etree.Element(
+                tags.import_,
+                namespace=name.namespace, schemaLocation=name.namespace)
+            self.visit_import(import_node, None)
+
+        return name
+
+    def _pop_annotation(self, items):
+        if not len(items):
+            return None, []
+
+        if items[0].tag == tags.annotation:
+            annotation = self.visit_annotation(items[0], None)
+            return annotation, items[1:]
+        return None, items
+
+    def _process_attributes(self, node, items):
+        attributes = []
+        for child in items:
+            attribute = self.process(child, node)
+            if child.tag in (tags.attribute, tags.attributeGroup, tags.anyAttribute):
+                attributes.append(attribute)
+            else:
+                raise XMLParseError("Unexpected tag: %s" % child.tag)
+        return attributes
+
+    visitors = {
+        tags.any: visit_any,
+        tags.element: visit_element,
+        tags.choice: visit_choice,
+        tags.simpleType: visit_simple_type,
+        tags.anyAttribute: visit_any_attribute,
+        tags.complexType: visit_complex_type,
+        tags.simpleContent: None,
+        tags.complexContent: None,
+        tags.sequence: visit_sequence,
+        tags.all: visit_all,
+        tags.group: visit_group,
+        tags.attribute: visit_attribute,
+        tags.import_: visit_import,
+        tags.include: visit_include,
+        tags.annotation: visit_annotation,
+        tags.attributeGroup: visit_attribute_group,
+        tags.notation: visit_notation,
+    }
+
+
+def _process_occurs_attrs(node):
+    """Process the min/max occurrence indicators"""
+    max_occurs = node.get('maxOccurs', '1')
+    min_occurs = int(node.get('minOccurs', '1'))
+    if max_occurs == 'unbounded':
+        max_occurs = 'unbounded'
+    else:
+        max_occurs = int(max_occurs)
+
+    return min_occurs, max_occurs
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..88d4e5d
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,2 @@
+from zeep.client import Client  # noqa
+from zeep.exceptions import Fault  # noqa
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..fc8d077
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,14 @@
+import pytest
+
+pytest.register_assert_rewrite('tests.utils')
+
+
+ at pytest.fixture(autouse=True)
+def no_requests(request, monkeypatch):
+    if request.node.get_marker('requests'):
+        return
+
+    def func(*args, **kwargs):
+        pytest.fail("External connections not allowed during tests.")
+
+    monkeypatch.setattr("socket.socket", func)
diff --git a/tests/integration/hello_world_recursive.wsdl b/tests/integration/hello_world_recursive.wsdl
new file mode 100644
index 0000000..d67624e
--- /dev/null
+++ b/tests/integration/hello_world_recursive.wsdl
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License. You may obtain a copy of the License at
+ 
+  http://www.apache.org/licenses/LICENSE-2.0
+ 
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied. See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<wsdl:definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+    xmlns:tns="http://apache.org/hello_world_soap_http"
+    xmlns:import="http://apache.org/hello_world_soap_http/import"
+    xmlns:x1="http://apache.org/hello_world_soap_http/types"
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    targetNamespace="http://apache.org/hello_world_soap_http" name="HelloWorld">
+    <wsdl:import namespace="http://apache.org/hello_world_soap_http/import" location="hello_world_recursive_import.wsdl"/>
+    <wsdl:types>
+        <schema targetNamespace="http://apache.org/hello_world_soap_http/types" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:x1="http://apache.org/hello_world_soap_http/types" elementFormDefault="qualified">
+            <element name="sayHi">
+                <complexType/>
+            </element>
+            <element name="sayHiResponse">
+                <complexType>
+                    <sequence>
+                        <element name="responseType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="greetMe">
+                <complexType>
+                    <sequence>
+                        <element name="requestType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="greetMeResponse">
+                <complexType>
+                    <sequence>
+                        <element name="responseType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="greetMeSometime">
+                <complexType>
+                    <sequence>
+                        <element name="requestType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="greetMeSometimeResponse">
+                <complexType>
+                    <sequence>
+                        <element name="responseType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="greetMeOneWay">
+                <complexType>
+                    <sequence>
+                        <element name="requestType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="testDocLitFault">
+                <complexType>
+                    <sequence>
+                        <element name="faultType" type="string"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="testDocLitFaultResponse">
+                <complexType>
+                    <sequence/>
+                </complexType>
+            </element>
+            <complexType name="ErrorCode">
+                <sequence>
+                    <element name="minor" type="short"/>
+                    <element name="major" type="short"/>
+                </sequence>
+            </complexType>
+            <element name="NoSuchCodeLit">
+                <complexType>
+                    <sequence>
+                        <element name="code" type="x1:ErrorCode"/>
+                    </sequence>
+                </complexType>
+            </element>
+            <element name="BadRecordLit" type="string"/>
+            <complexType name="BadRecord">
+                <sequence>
+                    <element name="reason" type="string"/>
+                    <element name="code" type="short"/>
+                </sequence>
+            </complexType>
+            <complexType name="addNumbers">
+                <sequence>
+                    <element name="arg0" type="int"/>
+                    <element name="arg1" type="int"/>
+                </sequence>
+            </complexType>
+            <element name="addNumbers" type="x1:addNumbers"/>
+            <complexType name="addNumbersResponse">
+                <sequence>
+                    <element name="return" type="int"/>
+                </sequence>
+            </complexType>
+            <element name="addNumbersResponse" type="x1:addNumbersResponse"/>
+            <element name="BareDocument" type="string"/>
+            <element name="BareDocumentResponse">
+                <complexType>
+                    <sequence>
+                        <element name="company" type="string"/>
+                    </sequence>
+                    <attribute name="id" type="int"/>
+                </complexType>
+            </element>      
+        </schema>
+    </wsdl:types>
+    <wsdl:message name="sayHiRequest">
+        <wsdl:part name="in" element="x1:sayHi"/>
+    </wsdl:message>
+    <wsdl:message name="sayHiResponse">
+        <wsdl:part name="out" element="x1:sayHiResponse"/>
+    </wsdl:message>
+    <wsdl:message name="greetMeRequest">
+        <wsdl:part name="in" element="x1:greetMe"/>
+    </wsdl:message>
+    <wsdl:message name="greetMeResponse">
+        <wsdl:part name="out" element="x1:greetMeResponse"/>
+    </wsdl:message>
+    <wsdl:message name="greetMeSometimeRequest">
+        <wsdl:part name="in" element="x1:greetMeSometime"/>
+    </wsdl:message>
+    <wsdl:message name="greetMeSometimeResponse">
+        <wsdl:part name="out" element="x1:greetMeSometimeResponse"/>
+    </wsdl:message>
+    <wsdl:message name="greetMeOneWayRequest">
+        <wsdl:part name="in" element="x1:greetMeOneWay"/>
+    </wsdl:message>
+    <wsdl:message name="testDocLitFaultRequest">
+        <wsdl:part name="in" element="x1:testDocLitFault"/>
+    </wsdl:message>
+    <wsdl:message name="testDocLitFaultResponse">
+        <wsdl:part name="out" element="x1:testDocLitFaultResponse"/>
+    </wsdl:message>
+    <wsdl:message name="NoSuchCodeLitFault">
+        <wsdl:part name="NoSuchCodeLit" element="x1:NoSuchCodeLit"/>
+    </wsdl:message>
+    <wsdl:message name="BadRecordLitFault">
+        <wsdl:part name="BadRecordLit" element="x1:BadRecordLit"/>
+    </wsdl:message>
+    <wsdl:message name="testDocLitBareRequest">
+        <wsdl:part name="in" element="x1:BareDocument"/>
+    </wsdl:message>
+    <wsdl:message name="testDocLitBareResponse">
+        <wsdl:part name="out" element="x1:BareDocumentResponse"/>
+    </wsdl:message> 
+    <wsdl:portType name="Greeter">
+        <wsdl:operation name="sayHi">
+            <wsdl:input name="sayHiRequest" message="tns:sayHiRequest"/>
+            <wsdl:output name="sayHiResponse" message="tns:sayHiResponse"/>
+        </wsdl:operation>
+        <wsdl:operation name="greetMe">
+            <wsdl:input name="greetMeRequest" message="tns:greetMeRequest"/>
+            <wsdl:output name="greetMeResponse" message="tns:greetMeResponse"/>
+        </wsdl:operation>
+       <wsdl:operation name="greetMeSometime">
+            <wsdl:input name="greetMeSometimeRequest" message="tns:greetMeSometimeRequest"/>
+            <wsdl:output name="greetMeSometimeResponse" message="tns:greetMeSometimeResponse"/>
+        </wsdl:operation>
+        <wsdl:operation name="greetMeOneWay">
+            <wsdl:input name="greetMeOneWayRequest" message="tns:greetMeOneWayRequest"/>
+        </wsdl:operation>
+        <wsdl:operation name="testDocLitFault">
+            <wsdl:input name="testDocLitFaultRequest" message="tns:testDocLitFaultRequest"/>
+            <wsdl:output name="testDocLitFaultResponse" message="tns:testDocLitFaultResponse"/>
+            <wsdl:fault name="NoSuchCodeLitFault" message="tns:NoSuchCodeLitFault"/>
+            <wsdl:fault name="BadRecordLitFault" message="tns:BadRecordLitFault"/>
+        </wsdl:operation>
+        <wsdl:operation name="testDocLitBare">
+            <wsdl:input name="testDocLitBareRequest" message="tns:testDocLitBareRequest"/>
+            <wsdl:output name="testDocLitBareResponse" message="tns:testDocLitBareResponse"/>
+        </wsdl:operation>       
+    </wsdl:portType>
+    
+    <wsdl:service name="SOAPService">
+        <wsdl:port name="SoapPort" binding="import:Greeter_SOAPBinding">
+            <soap:address location="http://localhost:9000/SoapContext/SoapPort"/>
+        </wsdl:port>
+    </wsdl:service>
+</wsdl:definitions>
+
diff --git a/tests/integration/hello_world_recursive_import.wsdl b/tests/integration/hello_world_recursive_import.wsdl
new file mode 100644
index 0000000..84be83b
--- /dev/null
+++ b/tests/integration/hello_world_recursive_import.wsdl
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License. You may obtain a copy of the License at
+ 
+  http://www.apache.org/licenses/LICENSE-2.0
+ 
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied. See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<wsdl:definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+    xmlns:tns="http://apache.org/hello_world_soap_http/import"
+    xmlns:x0="http://apache.org/hello_world_soap_http"
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+    targetNamespace="http://apache.org/hello_world_soap_http/import" name="HelloWorldImport">
+    
+    <wsdl:import namespace="http://apache.org/hello_world_soap_http" location="hello_world_recursive.wsdl"/>
+    
+    <wsdl:binding name="Greeter_SOAPBinding" type="x0:Greeter">
+        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+        <wsdl:operation name="sayHi">
+            <soap:operation style="document"/>
+            <wsdl:input>
+                <soap:body use="literal"/>
+            </wsdl:input>
+            <wsdl:output>
+                <soap:body use="literal"/>
+            </wsdl:output>
+        </wsdl:operation>
+        <wsdl:operation name="greetMe">
+            <soap:operation style="document"/>
+            <wsdl:input>
+                <soap:body use="literal"/>
+            </wsdl:input>
+            <wsdl:output>
+                <soap:body use="literal"/>
+            </wsdl:output>
+        </wsdl:operation>
+        <wsdl:operation name="greetMeSometime">
+            <soap:operation style="document"/>
+            <wsdl:input>
+                <soap:body use="literal"/>
+            </wsdl:input>
+            <wsdl:output>
+                <soap:body use="literal"/>
+            </wsdl:output>
+        </wsdl:operation>
+        <wsdl:operation name="greetMeOneWay">
+            <soap:operation style="document"/>
+            <wsdl:input>
+                <soap:body use="literal"/>
+            </wsdl:input>
+        </wsdl:operation>
+        <wsdl:operation name="testDocLitFault">
+            <soap:operation style="document"/>
+            <wsdl:input>
+                <soap:body use="literal"/>
+            </wsdl:input>
+            <wsdl:output>
+                <soap:body use="literal"/>
+            </wsdl:output>
+            <wsdl:fault name="NoSuchCodeLitFault">
+                <soap:fault name="NoSuchCodeLitFault" use="literal"/>
+            </wsdl:fault>
+            <wsdl:fault name="BadRecordLitFault">
+                <soap:fault name="BadRecordLitFault" use="literal"/>
+            </wsdl:fault>
+        </wsdl:operation>
+        <wsdl:operation name="testDocLitBare">
+            <soap:operation style="document" soapAction="http://apache.org/hello_world_soap_http/testDocLitBare"/>
+            <wsdl:input name="testDocLitBareRequest">
+                <soap:body use="literal"/>
+            </wsdl:input>
+            <wsdl:output name="testDocLitBareResponse">
+                <soap:body use="literal"/>
+            </wsdl:output>
+        </wsdl:operation>
+    </wsdl:binding>
+    
+</wsdl:definitions>
+
diff --git a/tests/integration/recursive_schema_a.xsd b/tests/integration/recursive_schema_a.xsd
new file mode 100644
index 0000000..85af13e
--- /dev/null
+++ b/tests/integration/recursive_schema_a.xsd
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://test.python-zeep.org/recursive/main/a">
+    <import namespace="http://test.python-zeep.org/recursive/main/b" schemaLocation="recursive_schema_b.xsd"/>
+</schema>
diff --git a/tests/integration/recursive_schema_b.xsd b/tests/integration/recursive_schema_b.xsd
new file mode 100644
index 0000000..1233320
--- /dev/null
+++ b/tests/integration/recursive_schema_b.xsd
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://test.python-zeep.org/recursive/main/b">
+    <import namespace="http://test.python-zeep.org/recursive/main/c" schemaLocation="recursive_schema_c.xsd"/>
+</schema>
diff --git a/tests/integration/recursive_schema_c.xsd b/tests/integration/recursive_schema_c.xsd
new file mode 100644
index 0000000..c231b1a
--- /dev/null
+++ b/tests/integration/recursive_schema_c.xsd
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://test.python-zeep.org/recursive/main/c">
+    <import namespace="http://test.python-zeep.org/recursive/main/a" schemaLocation="recursive_schema_a.xsd"/>
+</schema>
diff --git a/tests/integration/recursive_schema_main.wsdl b/tests/integration/recursive_schema_main.wsdl
new file mode 100644
index 0000000..8400cab
--- /dev/null
+++ b/tests/integration/recursive_schema_main.wsdl
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<wsdl:definitions xmlns="http://schemas.xmlsoap.org/wsdl/" 
+                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
+                  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
+                  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
+                  xmlns:tns="http://test.python-zeep.org/tests"
+                  targetNamespace="http://test.python-zeep.org/tests">
+  <wsdl:types>
+    <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://test.python-zeep.org/recursive/main" elementFormDefault="qualified">
+      <import namespace="http://test.python-zeep.org/recursive/main/a" schemaLocation="recursive_schema_a.xsd"/>
+    </schema>
+  </wsdl:types>
+  <wsdl:portType name="portje">
+    </wsdl:portType>
+    <wsdl:binding name="binding" type="portje">
+        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+    </wsdl:binding>
+    <wsdl:service name="SOAPService">
+        <wsdl:port name="zeepje" binding="tns:binding">
+            <soap:address location="http://localhost:9000/SoapContext/SoapPort"/>
+        </wsdl:port>
+    </wsdl:service>
+</wsdl:definitions>
diff --git a/tests/integration/test_hello_world_recursive.py b/tests/integration/test_hello_world_recursive.py
new file mode 100644
index 0000000..74661e1
--- /dev/null
+++ b/tests/integration/test_hello_world_recursive.py
@@ -0,0 +1,11 @@
+import os
+
+import zeep
+
+
+def test_hello_world():
+    path = os.path.join(
+        os.path.dirname(os.path.realpath(__file__)),
+        'hello_world_recursive.wsdl')
+    client = zeep.Client(path)
+    client.wsdl.dump()
diff --git a/tests/integration/test_recursive_schema.py b/tests/integration/test_recursive_schema.py
new file mode 100644
index 0000000..a8b2487
--- /dev/null
+++ b/tests/integration/test_recursive_schema.py
@@ -0,0 +1,11 @@
+import os
+
+import zeep
+
+
+def test_hello_world():
+    path = os.path.join(
+        os.path.dirname(os.path.realpath(__file__)),
+        'recursive_schema_main.wsdl')
+    client = zeep.Client(path)
+    client.wsdl.dump()
diff --git a/tests/test_cache.py b/tests/test_cache.py
new file mode 100644
index 0000000..66befce
--- /dev/null
+++ b/tests/test_cache.py
@@ -0,0 +1,46 @@
+import datetime
+
+import freezegun
+
+from zeep import cache
+
+
+def test_sqlite_cache(tmpdir):
+    c = cache.SqliteCache(path=tmpdir.join('sqlite.cache.db').strpath)
+    c.add('http://tests.python-zeep.org/example.wsdl', b'content')
+
+    result = c.get('http://tests.python-zeep.org/example.wsdl')
+    assert result == b'content'
+
+
+def test_sqlite_cache_timeout(tmpdir):
+    c = cache.SqliteCache(path=tmpdir.join('sqlite.cache.db').strpath)
+    c.add('http://tests.python-zeep.org/example.wsdl', b'content')
+    result = c.get('http://tests.python-zeep.org/example.wsdl')
+    assert result == b'content'
+
+    freeze_dt = datetime.datetime.utcnow() + datetime.timedelta(seconds=7200)
+    with freezegun.freeze_time(freeze_dt):
+        result = c.get('http://tests.python-zeep.org/example.wsdl')
+        assert result is None
+
+
+def test_memory_cache_timeout(tmpdir):
+    c = cache.InMemoryCache()
+    c.add('http://tests.python-zeep.org/example.wsdl', b'content')
+    result = c.get('http://tests.python-zeep.org/example.wsdl')
+    assert result == b'content'
+
+    freeze_dt = datetime.datetime.utcnow() + datetime.timedelta(seconds=7200)
+    with freezegun.freeze_time(freeze_dt):
+        result = c.get('http://tests.python-zeep.org/example.wsdl')
+        assert result is None
+
+
+def test_memory_cache_share_data(tmpdir):
+    a = cache.InMemoryCache()
+    b = cache.InMemoryCache()
+    a.add('http://tests.python-zeep.org/example.wsdl', b'content')
+
+    result = b.get('http://tests.python-zeep.org/example.wsdl')
+    assert result == b'content'
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 0000000..122abc3
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,254 @@
+import os
+
+import pytest
+import requests_mock
+from lxml import etree
+
+from zeep import client
+from zeep import xsd
+from zeep.exceptions import Error
+from tests.utils import load_xml
+
+
+def test_bind():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    service = client_obj.bind()
+    assert service
+
+
+def test_bind_service():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    service = client_obj.bind('StockQuoteService')
+    assert service
+
+
+def test_bind_service_port():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    service = client_obj.bind('StockQuoteService', 'StockQuotePort')
+    assert service
+
+
+def test_service_proxy_ok():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    assert client_obj.service.GetLastTradePrice
+
+
+def test_service_proxy_non_existing():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    with pytest.raises(AttributeError):
+        assert client_obj.service.NonExisting
+
+
+def test_client_no_wsdl():
+    with pytest.raises(ValueError):
+        client.Client(None)
+
+
+def test_client_cache_service():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    assert client_obj.service.GetLastTradePrice
+    assert client_obj.service.GetLastTradePrice
+
+
+def test_force_https():
+    with open('tests/wsdl_files/soap.wsdl') as fh:
+        response = fh.read()
+
+    with requests_mock.mock() as m:
+        url = 'https://tests.python-zeep.org/wsdl'
+        m.get(url, text=response, status_code=200)
+        client_obj = client.Client(url)
+        binding_options = client_obj.service._binding_options
+        assert binding_options['address'].startswith('https')
+
+        expected_url = 'https://example.com/stockquote'
+        assert binding_options['address'] == expected_url
+
+
+ at pytest.mark.requests
+def test_create_service():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    service = client_obj.create_service(
+        '{http://example.com/stockquote.wsdl}StockQuoteBinding',
+        'http://test.python-zeep.org/x')
+
+    response = """
+    <?xml version="1.0"?>
+    <soapenv:Envelope
+        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+        xmlns:stoc="http://example.com/stockquote.xsd">
+       <soapenv:Header/>
+       <soapenv:Body>
+          <stoc:TradePrice>
+             <price>120.123</price>
+          </stoc:TradePrice>
+       </soapenv:Body>
+    </soapenv:Envelope>
+    """.strip()
+
+    with requests_mock.mock() as m:
+        m.post('http://test.python-zeep.org/x', text=response)
+        result = service.GetLastTradePrice('foobar')
+        assert result == 120.123
+        assert m.request_history[0].headers['User-Agent'].startswith('Zeep/')
+        assert m.request_history[0].body.startswith(
+            b"<?xml version='1.0' encoding='utf-8'?>")
+
+
+def test_load_wsdl_with_file_prefix():
+    cwd = os.path.dirname(__file__)
+    client.Client(
+        'file://' + os.path.join(cwd, 'wsdl_files/soap.wsdl'))
+
+
+ at pytest.mark.requests
+def test_service_proxy():
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+
+    response = """
+    <?xml version="1.0"?>
+    <soapenv:Envelope
+        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+        xmlns:stoc="http://example.com/stockquote.xsd">
+       <soapenv:Header/>
+       <soapenv:Body>
+          <stoc:TradePrice>
+             <price>120.123</price>
+          </stoc:TradePrice>
+       </soapenv:Body>
+    </soapenv:Envelope>
+    """.strip()
+
+    with requests_mock.mock() as m:
+        m.post('http://example.com/stockquote', text=response)
+        result = client_obj.service.GetLastTradePrice('foobar')
+        assert result == 120.123
+
+
+ at pytest.mark.requests
+def test_call_method_fault():
+    obj = client.Client('tests/wsdl_files/soap.wsdl')
+
+    response = """
+        <?xml version="1.0" encoding="utf-8"?>
+        <soap:Envelope
+            xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+          <soap:Body>
+            <soap:Fault>
+              <faultcode>soap:Server</faultcode>
+              <faultstring>
+                Big fatal error!!
+              </faultstring>
+              <faultactor>StockListByDate</faultactor>
+              <detail>
+                <Error xmlns="http://sherpa.sherpaan.nl/Sherpa">
+                  <ErrorMessage>wrong security code</ErrorMessage>
+                  <ErrorSource>StockListByDate</ErrorSource>
+                </Error>
+              </detail>
+            </soap:Fault>
+          </soap:Body>
+        </soap:Envelope>
+    """.strip()
+
+    with requests_mock.mock() as m:
+        m.post('http://example.com/stockquote', text=response, status_code=500)
+        with pytest.raises(Error):
+            obj.service.GetLastTradePrice(tickerSymbol='foobar')
+
+
+def test_set_context_options_timeout():
+    obj = client.Client('tests/wsdl_files/soap.wsdl')
+
+    assert obj.transport.operation_timeout is None
+    with obj.options(timeout=120):
+        assert obj.transport.operation_timeout == 120
+
+        with obj.options(timeout=90):
+            assert obj.transport.operation_timeout == 90
+        assert obj.transport.operation_timeout == 120
+    assert obj.transport.operation_timeout is None
+
+
+ at pytest.mark.requests
+def test_default_soap_headers():
+    header = xsd.Element(None, xsd.ComplexType(
+        xsd.Sequence([
+            xsd.Element('{http://tests.python-zeep.org}name', xsd.String()),
+            xsd.Element('{http://tests.python-zeep.org}password', xsd.String()),
+        ])
+    ))
+    header_value = header(name='ik', password='geheim')
+
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    client_obj.set_default_soapheaders([header_value])
+
+    response = """
+    <?xml version="1.0"?>
+    <soapenv:Envelope
+        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+        xmlns:stoc="http://example.com/stockquote.xsd">
+       <soapenv:Header/>
+       <soapenv:Body>
+          <stoc:TradePrice>
+             <price>120.123</price>
+          </stoc:TradePrice>
+       </soapenv:Body>
+    </soapenv:Envelope>
+    """.strip()
+
+    with requests_mock.mock() as m:
+        m.post('http://example.com/stockquote', text=response)
+        client_obj.service.GetLastTradePrice('foobar')
+
+        doc = load_xml(m.request_history[0].body)
+        header = doc.find('{http://schemas.xmlsoap.org/soap/envelope/}Header')
+        assert header is not None
+        assert len(header.getchildren()) == 2
+
+
+ at pytest.mark.requests
+def test_default_soap_headers_extra():
+    header = xsd.Element(None, xsd.ComplexType(
+        xsd.Sequence([
+            xsd.Element('{http://tests.python-zeep.org}name', xsd.String()),
+            xsd.Element('{http://tests.python-zeep.org}password', xsd.String()),
+        ])
+    ))
+    header_value = header(name='ik', password='geheim')
+
+    extra_header = xsd.Element(None, xsd.ComplexType(
+        xsd.Sequence([
+            xsd.Element('{http://tests.python-zeep.org}name', xsd.String()),
+            xsd.Element('{http://tests.python-zeep.org}password', xsd.String()),
+        ])
+    ))
+    extra_header_value = extra_header(name='ik', password='geheim')
+
+    client_obj = client.Client('tests/wsdl_files/soap.wsdl')
+    client_obj.set_default_soapheaders([header_value])
+
+    response = """
+    <?xml version="1.0"?>
+    <soapenv:Envelope
+        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+        xmlns:stoc="http://example.com/stockquote.xsd">
+       <soapenv:Header/>
+       <soapenv:Body>
+          <stoc:TradePrice>
+             <price>120.123</price>
+          </stoc:TradePrice>
+       </soapenv:Body>
+    </soapenv:Envelope>
+    """.strip()
+
+    with requests_mock.mock() as m:
+        m.post('http://example.com/stockquote', text=response)
+        client_obj.service.GetLastTradePrice('foobar', _soapheaders=[extra_header_value])
+
+        doc = load_xml(m.request_history[0].body)
+        header = doc.find('{http://schemas.xmlsoap.org/soap/envelope/}Header')
+        assert header is not None
+        assert len(header.getchildren()) == 4
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
new file mode 100644
index 0000000..a3ddb06
--- /dev/null
+++ b/tests/test_helpers.py
@@ -0,0 +1,113 @@
+from lxml import etree
+
+from tests.utils import load_xml
+from zeep import xsd
+from zeep.helpers import serialize_object
+
+
+def test_serialize_simple():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'name'),
+                    xsd.String()),
+                xsd.Attribute(
+                    etree.QName('http://tests.python-zeep.org/', 'attr'),
+                    xsd.String()),
+            ])
+        ))
+
+    obj = custom_type(name='foo', attr='x')
+    assert obj.name == 'foo'
+    assert obj.attr == 'x'
+
+    result = serialize_object(obj)
+
+    assert result == {
+        'name': 'foo',
+        'attr': 'x',
+    }
+
+
+def test_serialize_nested_complex_type():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'items'),
+                    xsd.ComplexType(
+                        xsd.Sequence([
+                            xsd.Element(
+                                etree.QName('http://tests.python-zeep.org/', 'x'),
+                                xsd.String()),
+                            xsd.Element(
+                                etree.QName('http://tests.python-zeep.org/', 'y'),
+                                xsd.ComplexType(
+                                    xsd.Sequence([
+                                        xsd.Element(
+                                            etree.QName('http://tests.python-zeep.org/', 'x'),
+                                            xsd.String()),
+                                    ])
+                                )
+                            )
+                        ])
+                    ),
+                    max_occurs=2
+                )
+            ])
+        ))
+
+    obj = custom_type(
+        items=[
+            {'x': 'bla', 'y': {'x': 'deep'}},
+            {'x': 'foo', 'y': {'x': 'deeper'}},
+        ])
+
+    assert len(obj.items) == 2
+    obj.items[0].x == 'bla'
+    obj.items[0].y.x == 'deep'
+    obj.items[1].x == 'foo'
+    obj.items[1].y.x == 'deeper'
+
+    result = serialize_object(obj)
+
+    assert result == {
+        'items': [
+            {'x': 'bla', 'y': {'x': 'deep'}},
+            {'x': 'foo', 'y': {'x': 'deeper'}},
+        ]
+    }
+
+
+def test_nested_complex_types():
+    schema = xsd.Schema(load_xml("""
+        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <xsd:element name="container">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="item" type="tns:item"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:complexType name="item">
+            <xsd:sequence>
+              <xsd:element name="item_1" type="xsd:string"/>
+            </xsd:sequence>
+          </xsd:complexType>
+        </xsd:schema>
+    """))
+
+    container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
+    item_type = schema.get_type('{http://tests.python-zeep.org/}item')
+
+    instance = container_elm(item=item_type(item_1='foo'))
+    result = serialize_object(instance)
+    assert isinstance(result, dict), type(result)
+    assert isinstance(result['item'], dict), type(result['item'])
+    assert result['item']['item_1'] == 'foo'
diff --git a/tests/test_response.py b/tests/test_response.py
new file mode 100644
index 0000000..6364d85
--- /dev/null
+++ b/tests/test_response.py
@@ -0,0 +1,89 @@
+from lxml import etree
+
+from zeep.xsd import Schema
+
+
+def test_parse_response():
+    schema_node = etree.fromstring(b"""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+            xmlns="http://www.w3.org/2001/XMLSchema"
+            xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+            xmlns:tns="http://tests.python-zeep.org/">
+          <wsdl:types>
+            <schema targetNamespace="http://tests.python-zeep.org/"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+              <complexType name="Item">
+                <sequence>
+                  <element minOccurs="0" maxOccurs="1" name="Key" type="string" />
+                  <element minOccurs="1" maxOccurs="1" name="Value" type="int" />
+                </sequence>
+              </complexType>
+              <complexType name="ArrayOfItems">
+                <sequence>
+                  <element minOccurs="0" maxOccurs="unbounded" name="Item" nillable="true" type="tns:Item" />
+                </sequence>
+              </complexType>
+              <complexType name="ZeepExampleResult">
+                <sequence>
+                  <element minOccurs="1" maxOccurs="1" name="SomeValue" type="int" />
+                  <element minOccurs="0" maxOccurs="1" name="Results"
+                    type="tns:ArrayOfItems" />
+                </sequence>
+              </complexType>
+              <element name="ZeepExampleResponse">
+                <complexType>
+                  <sequence>
+                    <element minOccurs="0" maxOccurs="1" name="ZeepExampleResult" type="tns:ZeepExampleResult" />
+                  </sequence>
+                </complexType>
+              </element>
+            </schema>
+          </wsdl:types>
+        </wsdl:definitions>
+    """.strip())  # noqa
+
+    response_node = etree.fromstring(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <soap:Envelope
+            xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+          <soap:Body>
+            <ZeepExampleResponse xmlns="http://tests.python-zeep.org/">
+              <ZeepExampleResult>
+                <SomeValue>45313</SomeValue>
+                <Results>
+                  <Item>
+                    <Key>ABC100</Key>
+                    <Value>10</Value>
+                  </Item>
+                  <Item>
+                    <Key>ABC200</Key>
+                    <Value>20</Value>
+                  </Item>
+                </Results>
+              </ZeepExampleResult>
+            </ZeepExampleResponse>
+          </soap:Body>
+        </soap:Envelope>
+    """.strip())
+    schema = Schema(schema_node.find('*/{http://www.w3.org/2001/XMLSchema}schema'))
+    assert schema
+    response_type = schema.get_element(
+        '{http://tests.python-zeep.org/}ZeepExampleResponse')
+
+    nsmap = {
+        'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
+        'tns': 'http://tests.python-zeep.org/',
+    }
+    node = response_node.find('soap:Body/tns:ZeepExampleResponse', namespaces=nsmap)
+    assert node is not None
+    obj = response_type.parse(node, schema)
+    assert obj.ZeepExampleResult.SomeValue == 45313
+    assert len(obj.ZeepExampleResult.Results.Item) == 2
+    assert obj.ZeepExampleResult.Results.Item[0].Key == 'ABC100'
+    assert obj.ZeepExampleResult.Results.Item[0].Value == 10
+    assert obj.ZeepExampleResult.Results.Item[1].Key == 'ABC200'
+    assert obj.ZeepExampleResult.Results.Item[1].Value == 20
diff --git a/tests/test_wsdl.py b/tests/test_wsdl.py
new file mode 100644
index 0000000..86aa0eb
--- /dev/null
+++ b/tests/test_wsdl.py
@@ -0,0 +1,828 @@
+import io
+
+import pytest
+import requests_mock
+from lxml import etree
+from pretend import stub
+from six import StringIO
+
+from tests.utils import DummyTransport, assert_nodes_equal
+from zeep import wsdl
+from zeep import Client
+from zeep.transports import Transport
+
+
+ at pytest.mark.requests
+def test_parse_soap_wsdl():
+    client = Client('tests/wsdl_files/soap.wsdl', transport=Transport(),)
+
+    response = """
+        <?xml version="1.0"?>
+        <soapenv:Envelope
+            xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+            xmlns:stoc="http://example.com/stockquote.xsd">
+           <soapenv:Header/>
+           <soapenv:Body>
+              <stoc:TradePrice>
+                 <price>120.123</price>
+              </stoc:TradePrice>
+           </soapenv:Body>
+        </soapenv:Envelope>
+    """.strip()
+
+    client.set_ns_prefix('stoc', 'http://example.com/stockquote.xsd')
+
+    with requests_mock.mock() as m:
+        m.post('http://example.com/stockquote', text=response)
+        account_type = client.get_type('stoc:account')
+        account = account_type(id=100)
+        country = client.get_element('stoc:country').type()
+        country.name = 'The Netherlands'
+        country.code = 'NL'
+
+        result = client.service.GetLastTradePrice(
+            tickerSymbol='foobar',
+            account=account,
+            country=country)
+        assert result == 120.123
+
+        request = m.request_history[0]
+
+        # Compare request body
+        expected = """
+        <soap-env:Envelope
+                xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"
+                xmlns:stoc="http://example.com/stockquote.xsd">
+            <soap-env:Body>
+              <stoc:TradePriceRequest>
+                <tickerSymbol>foobar</tickerSymbol>
+                <account>
+                  <id>100</id>
+                  <user/>
+                </account>
+                <stoc:country>
+                  <name>The Netherlands</name>
+                  <code>NL</code>
+                </stoc:country>
+              </stoc:TradePriceRequest>
+           </soap-env:Body>
+        </soap-env:Envelope>
+        """
+        assert_nodes_equal(expected, request.body)
+
+
+ at pytest.mark.requests
+def test_parse_soap_header_wsdl():
+    client = Client('tests/wsdl_files/soap_header.wsdl', transport=Transport(),)
+
+    response = """
+    <?xml version="1.0"?>
+    <soapenv:Envelope
+        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
+        xmlns:stoc="http://example.com/stockquote.xsd">
+       <soapenv:Header/>
+       <soapenv:Body>
+          <stoc:TradePrice>
+             <price>120.123</price>
+          </stoc:TradePrice>
+       </soapenv:Body>
+    </soapenv:Envelope>
+    """.strip()
+
+    with requests_mock.mock() as m:
+        m.post('http://example.com/stockquote', text=response)
+        result = client.service.GetLastTradePrice(
+            tickerSymbol='foobar',
+            _soapheaders={
+                'header': {
+                    'username': 'ikke',
+                    'password': 'oeh-is-geheim!',
+                }
+            })
+
+        assert result == 120.123
+
+        request = m.request_history[0]
+
+        # Compare request body
+        expected = """
+        <soap-env:Envelope
+                xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
+           <soap-env:Header>
+              <ns0:Authentication xmlns:ns0="http://example.com/stockquote.xsd">
+                 <username>ikke</username>
+                 <password>oeh-is-geheim!</password>
+              </ns0:Authentication>
+           </soap-env:Header>
+           <soap-env:Body>
+              <ns0:TradePriceRequest xmlns:ns0="http://example.com/stockquote.xsd">
+                 <tickerSymbol>foobar</tickerSymbol>
+              </ns0:TradePriceRequest>
+           </soap-env:Body>
+        </soap-env:Envelope>
+        """
+        assert_nodes_equal(expected, request.body)
+
+
+def test_parse_types_multiple_schemas():
+
+    content = StringIO("""
+    <?xml version="1.0"?>
+    <wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+        xmlns:s1="http://microsoft.com/wsdl/types/"
+        xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+        xmlns:tns="http://tests.python-zeep.org/"
+        xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+        targetNamespace="http://tests.python-zeep.org/">
+      <wsdl:types>
+        <xsd:schema elementFormDefault="qualified"
+            xmlns:s1="http://microsoft.com/wsdl/types/"
+            xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+            xmlns:tns="http://tests.python-zeep.org//"
+            xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            targetNamespace="http://tests.python-zeep.org/">
+          <xsd:import namespace="http://microsoft.com/wsdl/types/" />
+          <xsd:element name="foobardiedar" type="s1:guid"/>
+        </xsd:schema>
+        <xsd:schema elementFormDefault="qualified"
+           targetNamespace="http://microsoft.com/wsdl/types/">
+          <xsd:simpleType name="guid">
+            <xsd:restriction base="xsd:string"/>
+          </xsd:simpleType>
+        </xsd:schema>
+      </wsdl:types>
+    </wsdl:definitions>
+    """.strip())
+
+    assert wsdl.Document(content, None)
+
+
+def test_parse_types_nsmap_issues():
+    content = StringIO("""
+    <?xml version="1.0"?>
+    <wsdl:definitions targetNamespace="urn:ec.europa.eu:taxud:vies:services:checkVat"
+      xmlns:tns1="urn:ec.europa.eu:taxud:vies:services:checkVat:types"
+      xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
+      xmlns:impl="urn:ec.europa.eu:taxud:vies:services:checkVat"
+      xmlns:apachesoap="http://xml.apache.org/xml-soap"
+      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+      xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/">
+      <wsdl:types>
+        <xsd:schema attributeFormDefault="qualified"
+            elementFormDefault="qualified"
+            targetNamespace="urn:ec.europa.eu:taxud:vies:services:checkVat:types"
+            xmlns="urn:ec.europa.eu:taxud:vies:services:checkVat:types">
+                <xsd:element name="checkVatApprox">
+                    <xsd:complexType>
+                        <xsd:sequence>
+                            <xsd:element maxOccurs="1" minOccurs="0"
+                                name="traderCompanyType"
+                                type="tns1:companyTypeCode"/>
+                        </xsd:sequence>
+                    </xsd:complexType>
+                </xsd:element>
+                <xsd:simpleType name="companyTypeCode">
+                    <xsd:restriction base="xsd:string">
+                        <xsd:pattern value="[A-Z]{2}\-[1-9][0-9]?"/>
+                    </xsd:restriction>
+                </xsd:simpleType>
+            </xsd:schema>
+      </wsdl:types>
+    </wsdl:definitions>
+    """.strip())
+    assert wsdl.Document(content, None)
+
+
+ at pytest.mark.requests
+def test_parse_soap_import_wsdl():
+    client = stub(transport=Transport(), wsse=None)
+    content = io.open(
+        'tests/wsdl_files/soap-enc.xsd', 'r', encoding='utf-8').read()
+
+    with requests_mock.mock() as m:
+        m.get('http://schemas.xmlsoap.org/soap/encoding/', text=content)
+
+        obj = wsdl.Document(
+            'tests/wsdl_files/soap_import_main.wsdl', transport=client.transport)
+        assert len(obj.services) == 1
+        assert obj.types.is_empty is False
+        obj.dump()
+
+
+def test_multiple_extension():
+    content = StringIO("""
+    <?xml version="1.0"?>
+    <wsdl:definitions
+      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+      xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/">
+      <wsdl:types>
+        <xs:schema
+            xmlns:xs="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/a"
+            targetNamespace="http://tests.python-zeep.org/a"
+            xmlns:b="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+
+            <xs:import namespace="http://tests.python-zeep.org/b"/>
+
+            <xs:complexType name="type_a">
+              <xs:complexContent>
+                <xs:extension base="b:type_b"/>
+              </xs:complexContent>
+            </xs:complexType>
+            <xs:element name="typetje" type="tns:type_a"/>
+        </xs:schema>
+
+        <xs:schema
+            xmlns:xs="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/b"
+            targetNamespace="http://tests.python-zeep.org/b"
+            xmlns:c="http://tests.python-zeep.org/c"
+            elementFormDefault="qualified">
+
+            <xs:import namespace="http://tests.python-zeep.org/c"/>
+
+            <xs:complexType name="type_b">
+              <xs:complexContent>
+                <xs:extension base="c:type_c"/>
+              </xs:complexContent>
+            </xs:complexType>
+        </xs:schema>
+        <xs:schema
+            xmlns:xs="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/c"
+            targetNamespace="http://tests.python-zeep.org/c"
+            elementFormDefault="qualified">
+
+            <xs:complexType name="type_c">
+              <xs:complexContent>
+                <xs:extension base="tns:type_d"/>
+              </xs:complexContent>
+            </xs:complexType>
+
+            <xs:complexType name="type_d">
+                <xs:attribute name="wat" type="xs:string" />
+            </xs:complexType>
+        </xs:schema>
+      </wsdl:types>
+    </wsdl:definitions>
+    """.strip())
+    document = wsdl.Document(content, None)
+
+    type_a = document.types.get_element('ns0:typetje')
+    type_a(wat='x')
+
+    type_a = document.types.get_type('ns0:type_a')
+    type_a(wat='x')
+
+
+def test_create_import_schema(recwarn):
+    content = StringIO("""
+    <?xml version="1.0"?>
+    <wsdl:definitions
+      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+      xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/">
+
+      <wsdl:types>
+        <xsd:schema>
+          <xsd:import namespace="http://tests.python-zeep.org/a"
+                      schemaLocation="a.xsd"/>
+        </xsd:schema>
+        <xsd:schema>
+          <xsd:import namespace="http://tests.python-zeep.org/b"
+                      schemaLocation="b.xsd"/>
+        </xsd:schema>
+      </wsdl:types>
+    </wsdl:definitions>
+    """.strip())
+
+    schema_node_a = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/a"
+            targetNamespace="http://tests.python-zeep.org/a"
+            xmlns:b="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+        </xsd:schema>
+    """.strip())
+
+    schema_node_b = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/b"
+            targetNamespace="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+
+            <xsd:element name="global" type="xsd:string"/>
+        </xsd:schema>
+    """.strip())
+
+    transport = DummyTransport()
+    transport.bind('a.xsd', schema_node_a)
+    transport.bind('b.xsd', schema_node_b)
+
+    document = wsdl.Document(content, transport)
+    assert len(recwarn) == 0
+    assert document.types.get_element('{http://tests.python-zeep.org/b}global')
+
+
+def test_wsdl_imports_xsd(recwarn):
+    content = StringIO("""
+    <?xml version="1.0"?>
+    <wsdl:definitions
+      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+      xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/">
+      <wsdl:import location="a.xsd" namespace="http://tests.python-zeep.org/a"/>
+    </wsdl:definitions>
+    """.strip())
+
+    schema_node_a = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/a"
+            targetNamespace="http://tests.python-zeep.org/a"
+            xmlns:b="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+          <xsd:import namespace="http://tests.python-zeep.org/b" schemaLocation="b.xsd"/>
+        </xsd:schema>
+    """.strip())
+
+    schema_node_b = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/b"
+            targetNamespace="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+        </xsd:schema>
+    """.strip())
+
+    transport = DummyTransport()
+    transport.bind('a.xsd', schema_node_a)
+    transport.bind('b.xsd', schema_node_b)
+
+    wsdl.Document(content, transport)
+
+
+def test_import_schema_without_location(recwarn):
+    content = StringIO("""
+    <?xml version="1.0"?>
+    <wsdl:definitions
+      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+      xmlns:b="http://tests.python-zeep.org/b"
+      xmlns:c="http://tests.python-zeep.org/c"
+      xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+      targetNamespace="http://tests.python-zeep.org/transient"
+      xmlns:tns="http://tests.python-zeep.org/transient">
+
+      <wsdl:types>
+        <xsd:schema>
+          <xsd:import namespace="http://tests.python-zeep.org/a"
+                      schemaLocation="a.xsd"/>
+        </xsd:schema>
+        <xsd:schema targetNamespace="http://tests.python-zeep.org/c">
+          <xsd:element name="bar" type="b:foo"/>
+        </xsd:schema>
+      </wsdl:types>
+      <wsdl:message name="method">
+        <wsdl:part name="param" element="c:bar"/>
+      </wsdl:message>
+      <wsdl:portType name="port_type">
+        <wsdl:operation name="method" parameterOrder="param">
+          <wsdl:input message="tns:method" />
+        </wsdl:operation>
+      </wsdl:portType>
+      <wsdl:binding name="binding" type="tns:port_type" >
+        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
+        <wsdl:operation name="method" >
+          <soap:operation soapAction="method"/>
+          <wsdl:input>
+            <soap:body use="literal" />
+          </wsdl:input>
+        </wsdl:operation>
+      </wsdl:binding>
+    </wsdl:definitions>
+    """.strip())
+
+    schema_node_a = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/a"
+            targetNamespace="http://tests.python-zeep.org/a"
+            xmlns:b="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+
+          <xsd:import namespace="http://tests.python-zeep.org/b"
+                      schemaLocation="b.xsd"/>
+
+        </xsd:schema>
+    """.strip())
+
+    schema_node_b = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/b"
+            targetNamespace="http://tests.python-zeep.org/b"
+            elementFormDefault="qualified">
+
+          <xsd:complexType name="foo">
+            <xsd:sequence>
+              <xsd:element name="item_1" type="xsd:string"/>
+            </xsd:sequence>
+          </xsd:complexType>
+        </xsd:schema>
+    """.strip())
+
+    transport = DummyTransport()
+    transport.bind('a.xsd', schema_node_a)
+    transport.bind('b.xsd', schema_node_b)
+
+    document = wsdl.Document(content, transport)
+    assert len(recwarn) == 0
+    assert document.types.get_type('{http://tests.python-zeep.org/b}foo')
+
+
+def test_wsdl_import(recwarn):
+    wsdl_main = StringIO("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/xsd-main"
+          xmlns:sec="http://tests.python-zeep.org/wsdl-secondary"
+          xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/xsd-main">
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-secondary"
+            location="http://tests.python-zeep.org/schema-2.wsdl"/>
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-main"
+                xmlns:tns="http://tests.python-zeep.org/xsd-main">
+              <xsd:element name="input" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-1">
+            <wsdl:part name="response" element="tns:input"/>
+          </wsdl:message>
+
+          <wsdl:portType name="TestPortType">
+            <wsdl:operation name="TestOperation1">
+              <wsdl:input message="message-1"/>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation2">
+              <wsdl:input message="sec:message-2"/>
+            </wsdl:operation>
+          </wsdl:portType>
+
+          <wsdl:binding name="TestBinding" type="tns:TestPortType">
+            <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+            <wsdl:operation name="TestOperation1">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation2">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+          </wsdl:binding>
+          <wsdl:service name="TestService">
+            <wsdl:documentation>Test service</wsdl:documentation>
+            <wsdl:port name="TestPortType" binding="tns:TestBinding">
+              <soap:address location="http://tests.python-zeep.org/test"/>
+            </wsdl:port>
+          </wsdl:service>
+        </wsdl:definitions>
+    """.strip())
+
+    wsdl_2 = ("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/wsdl-secondary"
+          xmlns:mine="http://tests.python-zeep.org/xsd-secondary"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/wsdl-secondary">
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-secondary"
+                xmlns:tns="http://tests.python-zeep.org/xsd-secondary">
+              <xsd:element name="input2" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-2">
+            <wsdl:part name="response" element="mine:input2"/>
+          </wsdl:message>
+        </wsdl:definitions>
+    """.strip())
+
+    transport = DummyTransport()
+    transport.bind('http://tests.python-zeep.org/schema-2.wsdl', wsdl_2)
+    document = wsdl.Document(wsdl_main, transport)
+    document.dump()
+
+
+def test_wsdl_import_transitive(recwarn):
+    wsdl_main = StringIO("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/xsd-main"
+          xmlns:sec="http://tests.python-zeep.org/wsdl-2"
+          xmlns:third="http://tests.python-zeep.org/wsdl-3"
+          xmlns:fourth="http://tests.python-zeep.org/wsdl-4"
+          xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/xsd-main">
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-2"
+            location="http://tests.python-zeep.org/schema-2.wsdl"/>
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-main"
+                xmlns:tns="http://tests.python-zeep.org/xsd-main">
+              <xsd:element name="input" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-1">
+            <wsdl:part name="response" element="tns:input"/>
+          </wsdl:message>
+
+          <wsdl:portType name="TestPortType">
+            <wsdl:operation name="TestOperation1">
+              <wsdl:input message="message-1"/>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation2">
+              <wsdl:input message="sec:message-2"/>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation3">
+              <wsdl:input message="third:message-3"/>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation4">
+              <wsdl:input message="fourth:message-4"/>
+            </wsdl:operation>
+          </wsdl:portType>
+
+          <wsdl:binding name="TestBinding" type="tns:TestPortType">
+            <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+            <wsdl:operation name="TestOperation1">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation2">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation3">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+          </wsdl:binding>
+          <wsdl:service name="TestService">
+            <wsdl:documentation>Test service</wsdl:documentation>
+            <wsdl:port name="TestPortType" binding="tns:TestBinding">
+              <soap:address location="http://tests.python-zeep.org/test"/>
+            </wsdl:port>
+          </wsdl:service>
+        </wsdl:definitions>
+    """.strip())
+
+    wsdl_2 = ("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/wsdl-2"
+          xmlns:mine="http://tests.python-zeep.org/xsd-2"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/wsdl-2">
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-3"
+            location="http://tests.python-zeep.org/schema-3.wsdl"/>
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-2"
+                xmlns:tns="http://tests.python-zeep.org/xsd-2">
+              <xsd:element name="input2" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-2">
+            <wsdl:part name="response" element="mine:input2"/>
+          </wsdl:message>
+        </wsdl:definitions>
+    """.strip())
+
+    wsdl_3 = ("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/wsdl-third"
+          xmlns:mine="http://tests.python-zeep.org/xsd-3"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/wsdl-3">
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-2"
+            location="http://tests.python-zeep.org/schema-2.wsdl"/>
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-4"
+            location="http://tests.python-zeep.org/schema-4.wsdl"/>
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-3"
+                xmlns:tns="http://tests.python-zeep.org/xsd-3">
+              <xsd:element name="input3" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-3">
+            <wsdl:part name="response" element="mine:input3"/>
+          </wsdl:message>
+        </wsdl:definitions>
+    """.strip())
+
+    wsdl_4 = ("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/wsdl-4"
+          xmlns:mine="http://tests.python-zeep.org/xsd-4"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/wsdl-4">
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-3"
+            location="http://tests.python-zeep.org/schema-3.wsdl"/>
+          <wsdl:message name="message-4">
+            <wsdl:part name="response" type="xsd:string"/>
+          </wsdl:message>
+        </wsdl:definitions>
+    """.strip())
+
+    transport = DummyTransport()
+    transport.bind('http://tests.python-zeep.org/schema-2.wsdl', wsdl_2)
+    transport.bind('http://tests.python-zeep.org/schema-3.wsdl', wsdl_3)
+    transport.bind('http://tests.python-zeep.org/schema-4.wsdl', wsdl_4)
+
+    document = wsdl.Document(wsdl_main, transport)
+    document.dump()
+
+
+def test_wsdl_import_xsd_references(recwarn):
+    wsdl_main = StringIO("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/xsd-main"
+          xmlns:sec="http://tests.python-zeep.org/wsdl-secondary"
+          xmlns:xsd-sec="http://tests.python-zeep.org/xsd-secondary"
+          xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/xsd-main">
+          <wsdl:import namespace="http://tests.python-zeep.org/wsdl-secondary"
+            location="http://tests.python-zeep.org/schema-2.wsdl"/>
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-main"
+                xmlns:tns="http://tests.python-zeep.org/xsd-main">
+              <xsd:element name="input" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-1">
+            <wsdl:part name="response" element="tns:input"/>
+          </wsdl:message>
+          <wsdl:message name="message-2">
+            <wsdl:part name="response" element="xsd-sec:input2"/>
+          </wsdl:message>
+
+          <wsdl:portType name="TestPortType">
+            <wsdl:operation name="TestOperation1">
+              <wsdl:input message="message-1"/>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation2">
+              <wsdl:input message="sec:message-2"/>
+            </wsdl:operation>
+          </wsdl:portType>
+
+          <wsdl:binding name="TestBinding" type="tns:TestPortType">
+            <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+            <wsdl:operation name="TestOperation1">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+            <wsdl:operation name="TestOperation2">
+              <soap:operation soapAction=""/>
+              <wsdl:input>
+                <soap:body use="literal"/>
+              </wsdl:input>
+            </wsdl:operation>
+          </wsdl:binding>
+          <wsdl:service name="TestService">
+            <wsdl:documentation>Test service</wsdl:documentation>
+            <wsdl:port name="TestPortType" binding="tns:TestBinding">
+              <soap:address location="http://tests.python-zeep.org/test"/>
+            </wsdl:port>
+          </wsdl:service>
+        </wsdl:definitions>
+    """.strip())
+
+    wsdl_2 = ("""
+        <?xml version="1.0"?>
+        <wsdl:definitions
+          xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          xmlns:tns="http://tests.python-zeep.org/wsdl-secondary"
+          xmlns:mine="http://tests.python-zeep.org/xsd-secondary"
+          xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
+          targetNamespace="http://tests.python-zeep.org/wsdl-secondary">
+          <wsdl:types>
+            <xsd:schema
+                targetNamespace="http://tests.python-zeep.org/xsd-secondary"
+                xmlns:tns="http://tests.python-zeep.org/xsd-secondary">
+              <xsd:element name="input2" type="xsd:string"/>
+            </xsd:schema>
+          </wsdl:types>
+          <wsdl:message name="message-2">
+            <wsdl:part name="response" element="mine:input2"/>
+          </wsdl:message>
+        </wsdl:definitions>
+    """.strip())
+
+    transport = DummyTransport()
+    transport.bind('http://tests.python-zeep.org/schema-2.wsdl', wsdl_2)
+    document = wsdl.Document(wsdl_main, transport)
+    document.dump()
+
+
+def test_parse_operation_empty_nodes():
+    content = StringIO("""
+        <?xml version="1.0"?>
+        <wsdl:definitions xmlns:s="http://www.w3.org/2001/XMLSchema"
+            xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
+            xmlns:tns="http://tests.python-zeep.org/"
+            xmlns:s1="http://microsoft.com/wsdl/types/"
+            xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+            xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
+            targetNamespace="http://tests.python-zeep.org/"
+            xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
+          <wsdl:types>
+            <s:schema targetNamespace="http://tests.python-zeep.org/">
+              <s:import namespace="http://microsoft.com/wsdl/types/" />
+              <s:element name="ExampleMethod">
+              </s:element>
+            </s:schema>
+            <s:schema targetNamespace="http://microsoft.com/wsdl/types/">
+              <s:simpleType name="char">
+                <s:restriction base="s:unsignedShort" />
+              </s:simpleType>
+            </s:schema>
+          </wsdl:types>
+          <wsdl:message name="MessageIn">
+            <wsdl:part name="parameters" element="tns:ExampleMethod" />
+          </wsdl:message>
+          <wsdl:message name="MessageOut">
+            <wsdl:part name="parameters" element="tns:ExampleMethod" />
+          </wsdl:message>
+          <wsdl:portType name="ExampleSoap">
+            <wsdl:operation name="ExampleMethod">
+              <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
+                Example documentation.
+              </wsdl:documentation>
+              <wsdl:input message="tns:MessageIn" />
+              <wsdl:output message="tns:MessageOut" />
+            </wsdl:operation>
+          </wsdl:portType>
+          <wsdl:binding name="ExampleSoap" type="tns:ExampleSoap">
+            <http:binding verb="POST" />
+            <wsdl:operation name="ExampleMethod">
+              <http:operation location="/ExampleMethod" />
+              <wsdl:input>
+                <mime:content type="application/x-www-form-urlencoded" />
+              </wsdl:input>
+              <wsdl:output />
+            </wsdl:operation>
+          </wsdl:binding>
+        </wsdl:definitions>
+    """.strip())
+
+    assert wsdl.Document(content, None)
diff --git a/tests/test_xsd.py b/tests/test_xsd.py
new file mode 100644
index 0000000..311f082
--- /dev/null
+++ b/tests/test_xsd.py
@@ -0,0 +1,587 @@
+import pytest
+from lxml import etree
+
+from tests.utils import assert_nodes_equal, render_node
+from zeep import xsd
+
+
+def test_container_elements():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'username'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'password'),
+                    xsd.String()),
+                xsd.Any(),
+            ])
+        ))
+
+    # sequences
+    custom_type(username='foo', password='bar')
+
+
+def test_create_node():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'username'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'password'),
+                    xsd.String()),
+            ]),
+            [
+                xsd.Attribute('attr', xsd.String()),
+            ]
+        ))
+
+    # sequences
+    obj = custom_type(username='foo', password='bar', attr='x')
+
+    expected = """
+      <document>
+        <ns0:authentication xmlns:ns0="http://tests.python-zeep.org/" attr="x">
+          <ns0:username>foo</ns0:username>
+          <ns0:password>bar</ns0:password>
+        </ns0:authentication>
+      </document>
+    """
+    node = etree.Element('document')
+    custom_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_element_simple_type():
+    elm = xsd.Element(
+        '{http://tests.python-zeep.org/}item', xsd.String())
+    obj = elm('foo')
+
+    expected = """
+      <document>
+        <ns0:item xmlns:ns0="http://tests.python-zeep.org/">foo</ns0:item>
+      </document>
+    """
+    node = etree.Element('document')
+    elm.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_nil_elements():
+    custom_type = xsd.Element(
+        '{http://tests.python-zeep.org/}container',
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    '{http://tests.python-zeep.org/}item_1',
+                    xsd.ComplexType(
+                        xsd.Sequence([
+                            xsd.Element(
+                                '{http://tests.python-zeep.org/}item_1_1',
+                                xsd.String())
+                        ]),
+                    ),
+                    nillable=True),
+                xsd.Element(
+                    '{http://tests.python-zeep.org/}item_2',
+                    xsd.DateTime(), nillable=True),
+                xsd.Element(
+                    '{http://tests.python-zeep.org/}item_3',
+                    xsd.String(), min_occurs=0, nillable=False),
+                xsd.Element(
+                    '{http://tests.python-zeep.org/}item_4',
+                    xsd.ComplexType(
+                        xsd.Sequence([
+                            xsd.Element(
+                                '{http://tests.python-zeep.org/}item_4_1',
+                                xsd.String(), nillable=True)
+                        ])
+                    )
+                ),
+            ])
+        ))
+    obj = custom_type(item_1=None, item_2=None, item_3=None, item_4={})
+
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
+          <ns0:item_2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
+          <ns0:item_4>
+            <ns0:item_4_1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
+          </ns0:item_4>
+        </ns0:container>
+      </document>
+    """
+    node = render_node(custom_type, obj)
+    etree.cleanup_namespaces(node)
+    assert_nodes_equal(expected, node)
+
+
+def test_invalid_kwarg():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'username'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'password'),
+                    xsd.String()),
+            ])
+        ))
+
+    with pytest.raises(TypeError):
+        custom_type(something='is-wrong')
+
+
+def test_invalid_kwarg_simple_type():
+    elm = xsd.Element(
+        '{http://tests.python-zeep.org/}item', xsd.String())
+
+    with pytest.raises(TypeError):
+        elm(something='is-wrong')
+
+
+def test_group_mixed():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'username'),
+                    xsd.String()),
+                xsd.Group(
+                    etree.QName('http://tests.python-zeep.org/', 'groupie'),
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'password'),
+                            xsd.String(),
+                        )
+                    ])
+                )
+            ])
+        ))
+    assert custom_type.signature()
+    obj = custom_type(username='foo', password='bar')
+
+    expected = """
+      <document>
+        <ns0:authentication xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:username>foo</ns0:username>
+          <ns0:password>bar</ns0:password>
+        </ns0:authentication>
+      </document>
+    """
+    node = etree.Element('document')
+    custom_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_any():
+    some_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'doei'),
+        xsd.String())
+
+    complex_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'complex'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.String()),
+            ])
+        ))
+
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'hoi'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Any(),
+                xsd.Any(),
+                xsd.Any(),
+            ])
+        ))
+
+    any_1 = xsd.AnyObject(some_type, "DOEI!")
+    any_2 = xsd.AnyObject(
+        complex_type, complex_type(item_1='val_1', item_2='val_2'))
+    any_3 = xsd.AnyObject(
+        complex_type, [
+            complex_type(item_1='val_1_1', item_2='val_1_2'),
+            complex_type(item_1='val_2_1', item_2='val_2_2'),
+        ])
+
+    obj = custom_type(_value_1=any_1, _value_2=any_2, _value_3=any_3)
+
+    expected = """
+      <document>
+        <ns0:hoi xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:doei>DOEI!</ns0:doei>
+          <ns0:complex>
+            <ns0:item_1>val_1</ns0:item_1>
+            <ns0:item_2>val_2</ns0:item_2>
+          </ns0:complex>
+          <ns0:complex>
+            <ns0:item_1>val_1_1</ns0:item_1>
+            <ns0:item_2>val_1_2</ns0:item_2>
+          </ns0:complex>
+          <ns0:complex>
+            <ns0:item_1>val_2_1</ns0:item_1>
+            <ns0:item_2>val_2_2</ns0:item_2>
+          </ns0:complex>
+        </ns0:hoi>
+      </document>
+    """
+    node = etree.Element('document')
+    custom_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_any_type_check():
+    some_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'doei'),
+        xsd.String())
+
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'complex'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Any(),
+            ])
+        ))
+    with pytest.raises(TypeError):
+        custom_type(_any_1=some_type)
+
+
+def test_choice_init():
+    root = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'kies'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'pre'),
+                    xsd.String()),
+                xsd.Choice([
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_3'),
+                        xsd.String()),
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_4_1'),
+                            xsd.String()),
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_4_2'),
+                            xsd.String()),
+                    ])
+                ], max_occurs=4),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'post'),
+                    xsd.String()),
+            ])
+        )
+    )
+
+    obj = root(
+        pre='foo',
+        _value_1=[
+            {'item_1': 'value-1'},
+            {'item_2': 'value-2'},
+            {'item_1': 'value-3'},
+            {'item_4_1': 'value-4-1', 'item_4_2': 'value-4-2'},
+        ])
+
+    assert obj._value_1 == [
+        {'item_1': 'value-1'},
+        {'item_2': 'value-2'},
+        {'item_1': 'value-3'},
+        {'item_4_1': 'value-4-1', 'item_4_2': 'value-4-2'},
+    ]
+
+    node = etree.Element('document')
+    root.render(node, obj)
+    assert etree.tostring(node)
+
+    expected = """
+    <document>
+      <ns0:kies xmlns:ns0="http://tests.python-zeep.org/">
+        <ns0:pre>foo</ns0:pre>
+        <ns0:item_1>value-1</ns0:item_1>
+        <ns0:item_2>value-2</ns0:item_2>
+        <ns0:item_1>value-3</ns0:item_1>
+        <ns0:item_4_1>value-4-1</ns0:item_4_1>
+        <ns0:item_4_2>value-4-2</ns0:item_4_2>
+        <ns0:post/>
+      </ns0:kies>
+    </document>
+    """.strip()
+    assert_nodes_equal(expected, node)
+
+
+def test_choice_determinst():
+    root = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'kies'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Choice([
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                            xsd.String()),
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                            xsd.String()),
+                    ]),
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                            xsd.String()),
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                            xsd.String()),
+                    ]),
+                ])
+            ])
+        )
+    )
+
+    obj = root(item_1='item-1', item_2='item-2')
+    node = etree.Element('document')
+    root.render(node, obj)
+    assert etree.tostring(node)
+
+    expected = """
+    <document>
+      <ns0:kies xmlns:ns0="http://tests.python-zeep.org/">
+        <ns0:item_1>item-1</ns0:item_1>
+        <ns0:item_2>item-2</ns0:item_2>
+      </ns0:kies>
+    </document>
+    """.strip()
+    assert_nodes_equal(expected, node)
+
+
+def test_sequence():
+    root = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'container'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Sequence([
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                            xsd.String()),
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                            xsd.String()),
+                    ], min_occurs=2, max_occurs=2),
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_3'),
+                            xsd.String()),
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_4'),
+                            xsd.String()),
+                    ]),
+                ])
+            ])
+        )
+    )
+    root(
+        _value_1=[
+            {
+                'item_1': 'foo',
+                'item_2': 'bar',
+            },
+            {
+                'item_1': 'foo',
+                'item_2': 'bar',
+            },
+        ],
+        item_3='foo',
+        item_4='bar',
+    )
+
+
+def test_mixed_choice():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.String()),
+                xsd.Sequence([
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_3'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_4'),
+                        xsd.String()),
+                ]),
+                xsd.Choice([
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_5'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_6'),
+                        xsd.String()),
+                    xsd.Sequence([
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_7'),
+                            xsd.String()),
+                        xsd.Element(
+                            etree.QName('http://tests.python-zeep.org/', 'item_8'),
+                            xsd.String()),
+                    ])
+                ])
+            ])
+        ))
+
+    item = custom_type(
+        item_1='item-1',
+        item_2='item-2',
+        item_3='item-3',
+        item_4='item-4',
+        item_7='item-7',
+        item_8='item-8',
+    )
+
+    assert item.item_1 == 'item-1'
+    assert item.item_2 == 'item-2'
+    assert item.item_3 == 'item-3'
+    assert item.item_4 == 'item-4'
+    assert item.item_7 == 'item-7'
+    assert item.item_8 == 'item-8'
+
+
+def test_xsi():
+    org_type = xsd.Element(
+        '{https://tests.python-zeep.org/}original',
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element('username', xsd.String()),
+                xsd.Element('password', xsd.String()),
+            ])
+        )
+    )
+    alt_type = xsd.Element(
+        '{https://tests.python-zeep.org/}alternative',
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element('username', xsd.String()),
+                xsd.Element('password', xsd.String()),
+            ])
+        )
+    )
+    instance = alt_type(username='mvantellingen', password='geheim')
+    render_node(org_type, instance)
+
+
+def test_duplicate_element_names():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'container'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item'),
+                    xsd.String()),
+            ])
+        ))
+
+    # sequences
+    expected = 'item: xsd:string, item__1: xsd:string, item__2: xsd:string'
+    assert custom_type.signature() == expected
+    obj = custom_type(item='foo', item__1='bar', item__2='lala')
+
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item>foo</ns0:item>
+          <ns0:item>bar</ns0:item>
+          <ns0:item>lala</ns0:item>
+        </ns0:container>
+      </document>
+    """
+    node = render_node(custom_type, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_element_attribute_name_conflict():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'container'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item'),
+                    xsd.String()),
+            ]),
+            [
+                xsd.Attribute('foo', xsd.String()),
+                xsd.Attribute('item', xsd.String()),
+            ]
+        ))
+
+    # sequences
+    expected = 'item: xsd:string, foo: xsd:string, attr__item: xsd:string'
+    assert custom_type.signature() == expected
+    obj = custom_type(item='foo', foo='x', attr__item='bar')
+
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/" foo="x" item="bar">
+          <ns0:item>foo</ns0:item>
+        </ns0:container>
+      </document>
+    """
+    node = render_node(custom_type, obj)
+    assert_nodes_equal(expected, node)
+
+    obj = custom_type.parse(node.getchildren()[0], None)
+    assert obj.item == 'foo'
+    assert obj.foo == 'x'
+    assert obj.attr__item == 'bar'
+
+
+def test_attr_name():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'UserName'),
+                    xsd.String(),
+                    attr_name='username'),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'Password_x'),
+                    xsd.String(),
+                    attr_name='password'),
+            ])
+        ))
+
+    # sequences
+    custom_type(username='foo', password='bar')
diff --git a/tests/test_xsd_builtins.py b/tests/test_xsd_builtins.py
new file mode 100644
index 0000000..0d3da42
--- /dev/null
+++ b/tests/test_xsd_builtins.py
@@ -0,0 +1,340 @@
+import datetime
+from decimal import Decimal as D
+
+import isodate
+import pytest
+import pytz
+import six
+
+from zeep.xsd import builtins
+
+
+class TestString:
+
+    def test_xmlvalue(self):
+        instance = builtins.String()
+        result = instance.xmlvalue('foobar')
+        assert result == 'foobar'
+
+    def test_pythonvalue(self):
+        instance = builtins.String()
+        result = instance.pythonvalue('foobar')
+        assert result == 'foobar'
+
+
+class TestBoolean:
+
+    def test_xmlvalue(self):
+        instance = builtins.Boolean()
+        assert instance.xmlvalue(True) == 'true'
+        assert instance.xmlvalue(False) == 'false'
+        assert instance.xmlvalue(1) == 'true'
+        assert instance.xmlvalue(0) == 'false'
+
+    def test_pythonvalue(self):
+        instance = builtins.Boolean()
+        assert instance.pythonvalue('1') is True
+        assert instance.pythonvalue('true') is True
+        assert instance.pythonvalue('0') is False
+        assert instance.pythonvalue('false') is False
+
+
+class TestDecimal:
+
+    def test_xmlvalue(self):
+        instance = builtins.Decimal()
+        assert instance.xmlvalue(D('10.00')) == '10.00'
+        assert instance.xmlvalue(D('10.000002')) == '10.000002'
+        assert instance.xmlvalue(D('10.000002')) == '10.000002'
+        assert instance.xmlvalue(D('10')) == '10'
+        assert instance.xmlvalue(D('-10')) == '-10'
+
+    def test_pythonvalue(self):
+        instance = builtins.Decimal()
+        assert instance.pythonvalue('10') == D('10')
+        assert instance.pythonvalue('10.001') == D('10.001')
+        assert instance.pythonvalue('+10.001') == D('10.001')
+        assert instance.pythonvalue('-10.001') == D('-10.001')
+
+
+class TestFloat:
+
+    def test_xmlvalue(self):
+        instance = builtins.Float()
+        assert instance.xmlvalue(float(10)) == '10.0'
+        assert instance.xmlvalue(float(3.9999)) == '3.9999'
+        assert instance.xmlvalue(float('inf')) == 'INF'
+        assert instance.xmlvalue(float(12.78e-2)) == '0.1278'
+        if six.PY2:
+            assert instance.xmlvalue(float('1267.43233E12')) == '1.26743233E+15'
+        else:
+            assert instance.xmlvalue(float('1267.43233E12')) == '1267432330000000.0'
+
+    def test_pythonvalue(self):
+        instance = builtins.Float()
+        assert instance.pythonvalue('10') == float('10')
+        assert instance.pythonvalue('-1E4') == float('-1E4')
+        assert instance.pythonvalue('1267.43233E12') == float('1267.43233E12')
+        assert instance.pythonvalue('12.78e-2') == float('0.1278')
+        assert instance.pythonvalue('12') == float(12)
+        assert instance.pythonvalue('-0') == float(0)
+        assert instance.pythonvalue('0') == float(0)
+        assert instance.pythonvalue('INF') == float('inf')
+
+
+class TestDouble:
+
+    def test_xmlvalue(self):
+        instance = builtins.Double()
+        assert instance.xmlvalue(float(10)) == '10.0'
+        assert instance.xmlvalue(float(3.9999)) == '3.9999'
+        assert instance.xmlvalue(float(12.78e-2)) == '0.1278'
+
+    def test_pythonvalue(self):
+        instance = builtins.Double()
+        assert instance.pythonvalue('10') == float('10')
+        assert instance.pythonvalue('12') == float(12)
+        assert instance.pythonvalue('-0') == float(0)
+        assert instance.pythonvalue('0') == float(0)
+
+
+class TestDuration:
+
+    def test_xmlvalue(self):
+        instance = builtins.Duration()
+        value = isodate.parse_duration('P0Y1347M0D')
+        assert instance.xmlvalue(value) == 'P1347M'
+
+    def test_pythonvalue(self):
+        instance = builtins.Duration()
+        expected = isodate.parse_duration('P0Y1347M0D')
+        value = 'P0Y1347M0D'
+        assert instance.pythonvalue(value) == expected
+
+
+class TestDateTime:
+
+    def test_xmlvalue(self):
+        instance = builtins.DateTime()
+        value = datetime.datetime(2016, 3, 4, 21, 14, 42)
+        assert instance.xmlvalue(value) == '2016-03-04T21:14:42'
+
+        value = datetime.datetime(2016, 3, 4, 21, 14, 42, tzinfo=pytz.utc)
+        assert instance.xmlvalue(value) == '2016-03-04T21:14:42Z'
+
+        value = value.astimezone(pytz.timezone('Europe/Amsterdam'))
+        assert instance.xmlvalue(value) == '2016-03-04T22:14:42+01:00'
+
+    def test_pythonvalue(self):
+        instance = builtins.DateTime()
+        value = datetime.datetime(2016, 3, 4, 21, 14, 42)
+        assert instance.pythonvalue('2016-03-04T21:14:42') == value
+
+    def test_pythonvalue_invalid(self):
+        instance = builtins.DateTime()
+        with pytest.raises(ValueError):
+            assert instance.pythonvalue('  :  :  ')
+
+
+class TestTime:
+
+    def test_xmlvalue(self):
+        instance = builtins.Time()
+        value = datetime.time(21, 14, 42)
+        assert instance.xmlvalue(value) == '21:14:42'
+
+    def test_pythonvalue(self):
+        instance = builtins.Time()
+        value = datetime.time(21, 14, 42)
+        assert instance.pythonvalue('21:14:42') == value
+
+        value = datetime.time(21, 14, 42, 120000)
+        assert instance.pythonvalue('21:14:42.120') == value
+
+        value = isodate.parse_time('21:14:42.120+0200')
+        assert instance.pythonvalue('21:14:42.120+0200') == value
+
+    def test_pythonvalue_invalid(self):
+        instance = builtins.Time()
+        with pytest.raises(ValueError):
+            assert instance.pythonvalue(':')
+
+
+class TestDate:
+
+    def test_xmlvalue(self):
+        instance = builtins.Date()
+        value = datetime.datetime(2016, 3, 4)
+        assert instance.xmlvalue(value) == '2016-03-04'
+        assert instance.xmlvalue('2016-03-04') == '2016-03-04'
+        assert instance.xmlvalue('2016-04') == '2016-04'
+
+    def test_pythonvalue(self):
+        instance = builtins.Date()
+        assert instance.pythonvalue('2016-03-04') == datetime.date(2016, 3, 4)
+        assert instance.pythonvalue('2001-10-26+02:00') == datetime.date(2001, 10, 26)
+        assert instance.pythonvalue('2001-10-26Z') == datetime.date(2001, 10, 26)
+        assert instance.pythonvalue('2001-10-26+00:00') == datetime.date(2001, 10, 26)
+
+    def test_pythonvalue_invalid(self):
+        instance = builtins.Date()
+        # negative dates are not supported for datetime.date objects so lets
+        # hope no-one uses it for now..
+        with pytest.raises(ValueError):
+            assert instance.pythonvalue('-2001-10-26')
+        with pytest.raises(ValueError):
+            assert instance.pythonvalue('-20000-04-01')
+
+
+class TestgYearMonth:
+
+    def test_xmlvalue(self):
+        instance = builtins.gYearMonth()
+        assert instance.xmlvalue((2012, 10, None)) == '2012-10'
+        assert instance.xmlvalue((2012, 10, pytz.utc)) == '2012-10Z'
+
+    def test_pythonvalue(self):
+        instance = builtins.gYearMonth()
+        assert instance.pythonvalue('2001-10') == (2001, 10, None)
+        assert instance.pythonvalue('2001-10+02:00') == (2001, 10, pytz.FixedOffset(120))
+        assert instance.pythonvalue('2001-10Z') == (2001, 10, pytz.utc)
+        assert instance.pythonvalue('2001-10+00:00') == (2001, 10, pytz.utc)
+        assert instance.pythonvalue('-2001-10') == (-2001, 10, None)
+        assert instance.pythonvalue('-20001-10') == (-20001, 10, None)
+
+        with pytest.raises(builtins.ParseError):
+            assert instance.pythonvalue('10-10')
+
+
+class TestgYear:
+
+    def test_xmlvalue(self):
+        instance = builtins.gYear()
+        instance.xmlvalue((2001, None)) == '2001'
+        instance.xmlvalue((2001, pytz.utc)) == '2001Z'
+
+    def test_pythonvalue(self):
+        instance = builtins.gYear()
+        assert instance.pythonvalue('2001') == (2001, None)
+        assert instance.pythonvalue('2001+02:00') == (2001, pytz.FixedOffset(120))
+        assert instance.pythonvalue('2001Z') == (2001, pytz.utc)
+        assert instance.pythonvalue('2001+00:00') == (2001, pytz.utc)
+        assert instance.pythonvalue('-2001') == (-2001, None)
+        assert instance.pythonvalue('-20000') == (-20000, None)
+
+        with pytest.raises(builtins.ParseError):
+            assert instance.pythonvalue('99')
+
+
+class TestgMonthDay:
+
+    def test_xmlvalue(self):
+        instance = builtins.gMonthDay()
+        assert instance.xmlvalue((12, 30, None)) == '--12-30'
+
+    def test_pythonvalue(self):
+        instance = builtins.gMonthDay()
+        assert instance.pythonvalue('--05-01') == (5, 1, None)
+        assert instance.pythonvalue('--11-01Z') == (11, 1, pytz.utc)
+        assert instance.pythonvalue('--11-01+02:00') == (11, 1, pytz.FixedOffset(120))
+        assert instance.pythonvalue('--11-01-04:00') == (11, 1, pytz.FixedOffset(-240))
+        assert instance.pythonvalue('--11-15') == (11, 15, None)
+        assert instance.pythonvalue('--02-29') == (2, 29, None)
+
+        with pytest.raises(builtins.ParseError):
+            assert instance.pythonvalue('99')
+
+
+class TestgMonth:
+
+    def test_xmlvalue(self):
+        instance = builtins.gMonth()
+        assert instance.xmlvalue((12, None)) == '--12'
+
+    def test_pythonvalue(self):
+        instance = builtins.gMonth()
+        assert instance.pythonvalue('--05') == (5, None)
+        assert instance.pythonvalue('--11Z') == (11, pytz.utc)
+        assert instance.pythonvalue('--11+02:00') == (11, pytz.FixedOffset(120))
+        assert instance.pythonvalue('--11-04:00') == (11, pytz.FixedOffset(-240))
+        assert instance.pythonvalue('--11') == (11, None)
+        assert instance.pythonvalue('--02') == (2, None)
+
+        with pytest.raises(builtins.ParseError):
+            assert instance.pythonvalue('99')
+
+
+class TestgDay:
+
+    def test_xmlvalue(self):
+        instance = builtins.gDay()
+
+        value = (1, None)
+        assert instance.xmlvalue(value) == '---01'
+
+        value = (1, pytz.FixedOffset(120))
+        assert instance.xmlvalue(value) == '---01+02:00'
+
+        value = (1, pytz.FixedOffset(-240))
+        assert instance.xmlvalue(value) == '---01-04:00'
+
+    def test_pythonvalue(self):
+        instance = builtins.gDay()
+        assert instance.pythonvalue('---01') == (1, None)
+        assert instance.pythonvalue('---01Z') == (1, pytz.utc)
+        assert instance.pythonvalue('---01+02:00') == (1, pytz.FixedOffset(120))
+        assert instance.pythonvalue('---01-04:00') == (1, pytz.FixedOffset(-240))
+        assert instance.pythonvalue('---15') == (15, None)
+        assert instance.pythonvalue('---31') == (31, None)
+        with pytest.raises(builtins.ParseError):
+            assert instance.pythonvalue('99')
+
+
+class TestHexBinary:
+    def test_xmlvalue(self):
+        instance = builtins.HexBinary()
+        assert instance.xmlvalue(b'\xFF') == b'\xFF'
+
+    def test_pythonvalue(self):
+        instance = builtins.HexBinary()
+        assert instance.pythonvalue(b'\xFF') == b'\xFF'
+
+
+class TestBase64Binary:
+    def test_xmlvalue(self):
+        instance = builtins.Base64Binary()
+        assert instance.xmlvalue(b'hoi') == b'aG9p'
+
+    def test_pythonvalue(self):
+        instance = builtins.Base64Binary()
+        assert instance.pythonvalue(b'aG9p') == b'hoi'
+
+
+class TestAnyURI:
+    def test_xmlvalue(self):
+        instance = builtins.AnyURI()
+        assert instance.xmlvalue('http://test.python-zeep.org') == 'http://test.python-zeep.org'
+
+    def test_pythonvalue(self):
+        instance = builtins.AnyURI()
+        assert instance.pythonvalue('http://test.python-zeep.org') == 'http://test.python-zeep.org'
+
+
+class TestInteger:
+    def test_xmlvalue(self):
+        instance = builtins.Integer()
+        assert instance.xmlvalue(100) == '100'
+
+    def test_pythonvalue(self):
+        instance = builtins.Integer()
+        assert instance.pythonvalue('100') == 100
+
+
+class TestAnyType:
+    def test_xmlvalue(self):
+        instance = builtins.AnyType()
+        assert instance.xmlvalue('http://test.python-zeep.org') == 'http://test.python-zeep.org'
+
+    def test_pythonvalue(self):
+        instance = builtins.AnyType()
+        assert instance.pythonvalue('http://test.python-zeep.org') == 'http://test.python-zeep.org'
diff --git a/tests/test_xsd_integration.py b/tests/test_xsd_integration.py
new file mode 100644
index 0000000..06df987
--- /dev/null
+++ b/tests/test_xsd_integration.py
@@ -0,0 +1,904 @@
+import pytest
+from lxml import etree
+
+from tests.utils import assert_nodes_equal, load_xml
+from zeep import xsd
+
+
+def test_complex_type_nested_wrong_type():
+    schema = xsd.Schema(load_xml("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <element minOccurs="0" maxOccurs="1" name="item">
+                  <complexType>
+                    <sequence>
+                      <element name="x" type="integer"/>
+                      <element name="y" type="integer"/>
+                    </sequence>
+                  </complexType>
+                </element>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """))
+
+    container_elm = schema.get_element('ns0:container')
+    with pytest.raises(TypeError):
+        container_elm(item={'bar': 1})
+
+
+def test_element_with_annotation():
+    schema = xsd.Schema(load_xml("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <element name="Address" type="tns:AddressType">
+              <annotation>
+                  <documentation>HOI!</documentation>
+              </annotation>
+          </element>
+          <complexType name="AddressType">
+            <sequence>
+              <element minOccurs="0" maxOccurs="unbounded" name="foo" type="string" />
+            </sequence>
+          </complexType>
+        </schema>
+    """))
+
+    schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/')
+    address_type = schema.get_element('tns:Address')
+    address_type(foo='bar')
+
+
+def test_complex_type_parsexml():
+    schema = xsd.Schema(load_xml("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <element name="Address">
+            <complexType>
+              <sequence>
+                <element minOccurs="0" maxOccurs="1" name="foo" type="string" />
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """))
+
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+
+    input_node = load_xml("""
+        <Address xmlns="http://tests.python-zeep.org/">
+          <foo>bar</foo>
+        </Address>
+    """)
+
+    obj = address_type.parse(input_node, None)
+    assert obj.foo == 'bar'
+
+
+def test_array():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <element name="Address">
+            <complexType>
+              <sequence>
+                <element minOccurs="0" maxOccurs="unbounded" name="foo" type="string" />
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/')
+
+    address_type = schema.get_element('tns:Address')
+
+    obj = address_type()
+    assert obj.foo == []
+    obj.foo.append('foo')
+    obj.foo.append('bar')
+
+    expected = """
+        <document  xmlns:tns="http://tests.python-zeep.org/">
+          <tns:Address>
+            <tns:foo>foo</tns:foo>
+            <tns:foo>bar</tns:foo>
+          </tns:Address>
+        </document>
+    """
+    node = etree.Element('document', nsmap=schema._prefix_map_custom)
+    address_type.render(node, obj)
+    print(etree.tostring(node))
+    assert_nodes_equal(expected, node)
+
+
+def test_complex_type_unbounded_one():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <element name="Address">
+            <complexType>
+              <sequence>
+                <element minOccurs="0" maxOccurs="unbounded" name="foo" type="string" />
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+    obj = address_type(foo=['foo'])
+
+    expected = """
+        <document>
+          <ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
+            <ns0:foo>foo</ns0:foo>
+          </ns0:Address>
+        </document>
+    """
+
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_complex_type_unbounded_named():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <element name="Address" type="tns:AddressType" />
+          <complexType name="AddressType">
+            <sequence>
+              <element minOccurs="0" maxOccurs="unbounded" name="foo" type="string" />
+            </sequence>
+          </complexType>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+    obj = address_type()
+    assert obj.foo == []
+    obj.foo.append('foo')
+    obj.foo.append('bar')
+
+    expected = """
+        <document>
+          <ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
+            <ns0:foo>foo</ns0:foo>
+            <ns0:foo>bar</ns0:foo>
+          </ns0:Address>
+        </document>
+    """
+
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_complex_type_array_to_other_complex_object():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+          <xs:complexType name="Address">
+            <xs:sequence>
+              <xs:element minOccurs="0" maxOccurs="1" name="foo" type="xs:string" />
+            </xs:sequence>
+          </xs:complexType>
+          <xs:complexType name="ArrayOfAddress">
+            <xs:sequence>
+              <xs:element minOccurs="0" maxOccurs="unbounded" name="Address" nillable="true" type="Address" />
+            </xs:sequence>
+          </xs:complexType>
+          <xs:element name="ArrayOfAddress" type="ArrayOfAddress"/>
+        </xs:schema>
+    """.strip())  # noqa
+
+    schema = xsd.Schema(node)
+    address_array = schema.get_element('ArrayOfAddress')
+    obj = address_array()
+    assert obj.Address == []
+
+    obj.Address.append(schema.get_type('Address')(foo='foo'))
+    obj.Address.append(schema.get_type('Address')(foo='bar'))
+
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <ArrayOfAddress>
+            <Address>
+                <foo>foo</foo>
+            </Address>
+            <Address>
+                <foo>bar</foo>
+            </Address>
+        </ArrayOfAddress>
+    """.strip())
+
+
+def test_complex_type_init_kwargs():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="Address">
+            <complexType>
+              <sequence>
+                <element minOccurs="0" maxOccurs="1" name="NameFirst" type="string"/>
+                <element minOccurs="0" maxOccurs="1" name="NameLast" type="string"/>
+                <element minOccurs="0" maxOccurs="1" name="Email" type="string"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+    obj = address_type(
+        NameFirst='John', NameLast='Doe', Email='j.doe at example.com')
+    assert obj.NameFirst == 'John'
+    assert obj.NameLast == 'Doe'
+    assert obj.Email == 'j.doe at example.com'
+
+
+def test_complex_type_init_args():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="Address">
+            <complexType>
+              <sequence>
+                <element minOccurs="0" maxOccurs="1" name="NameFirst" type="string"/>
+                <element minOccurs="0" maxOccurs="1" name="NameLast" type="string"/>
+                <element minOccurs="0" maxOccurs="1" name="Email" type="string"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+    obj = address_type('John', 'Doe', 'j.doe at example.com')
+    assert obj.NameFirst == 'John'
+    assert obj.NameLast == 'Doe'
+    assert obj.Email == 'j.doe at example.com'
+
+
+def test_group():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+                   xmlns:tns="http://tests.python-zeep.org/"
+                   targetNamespace="http://tests.python-zeep.org/"
+                   elementFormDefault="qualified">
+
+          <xs:element name="Address">
+            <xs:complexType>
+              <xs:group ref="tns:Name" />
+            </xs:complexType>
+          </xs:element>
+
+          <xs:group name="Name">
+            <xs:sequence>
+              <xs:element name="first_name" type="xs:string" />
+              <xs:element name="last_name" type="xs:string" />
+            </xs:sequence>
+          </xs:group>
+
+        </xs:schema>
+    """.strip())
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+
+    obj = address_type(first_name='foo', last_name='bar')
+
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    expected = """
+        <document>
+            <ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
+                <ns0:first_name>foo</ns0:first_name>
+                <ns0:last_name>bar</ns0:last_name>
+            </ns0:Address>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_group_for_type():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+                   xmlns:tns="http://tests.python-zeep.org/"
+                   targetNamespace="http://tests.python-zeep.org/"
+                   elementFormDefault="unqualified">
+
+          <xs:element name="Address" type="tns:AddressType" />
+
+          <xs:complexType name="AddressType">
+            <xs:sequence>
+              <xs:group ref="tns:NameGroup"/>
+              <xs:group ref="tns:AddressGroup"/>
+            </xs:sequence>
+          </xs:complexType>
+
+          <xs:group name="NameGroup">
+            <xs:sequence>
+              <xs:element name="first_name" type="xs:string" />
+              <xs:element name="last_name" type="xs:string" />
+            </xs:sequence>
+          </xs:group>
+
+          <xs:group name="AddressGroup">
+            <xs:annotation>
+              <xs:documentation>blub</xs:documentation>
+            </xs:annotation>
+            <xs:sequence>
+              <xs:element name="city" type="xs:string" />
+              <xs:element name="country" type="xs:string" />
+            </xs:sequence>
+          </xs:group>
+        </xs:schema>
+    """.strip())
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+
+    obj = address_type(
+        first_name='foo', last_name='bar',
+        city='Utrecht', country='The Netherlands')
+
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    expected = """
+        <document>
+            <ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
+                <first_name>foo</first_name>
+                <last_name>bar</last_name>
+                <city>Utrecht</city>
+                <country>The Netherlands</country>
+            </ns0:Address>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_element_ref_missing_namespace():
+    # For buggy soap servers (#170)
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="foo" type="string"/>
+          <element name="bar">
+            <complexType>
+              <sequence>
+                <element ref="tns:foo"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+
+    custom_type = schema.get_element('{http://tests.python-zeep.org/}bar')
+    input_xml = load_xml("""
+            <ns0:bar xmlns:ns0="http://tests.python-zeep.org/">
+                <foo>bar</foo>
+            </ns0:bar>
+    """)
+    item = custom_type.parse(input_xml, schema)
+    assert item.foo == 'bar'
+
+
+def test_element_ref():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                   elementFormDefault="qualified">
+          <element name="foo" type="string"/>
+          <element name="bar">
+            <complexType>
+              <sequence>
+                <element ref="tns:foo"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+
+    foo_type = schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert isinstance(foo_type.type, xsd.String)
+
+    custom_type = schema.get_element('{http://tests.python-zeep.org/}bar')
+    custom_type.signature()
+    obj = custom_type(foo='bar')
+
+    node = etree.Element('document')
+    custom_type.render(node, obj)
+    expected = """
+        <document>
+            <ns0:bar xmlns:ns0="http://tests.python-zeep.org/">
+                <ns0:foo>bar</ns0:foo>
+            </ns0:bar>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_element_ref_occurs():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                   elementFormDefault="qualified">
+          <element name="foo" type="string"/>
+          <element name="bar">
+            <complexType>
+              <sequence>
+                <element ref="tns:foo" minOccurs="0"/>
+                <element name="bar" type="string"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+
+    foo_type = schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert isinstance(foo_type.type, xsd.String)
+
+    custom_type = schema.get_element('{http://tests.python-zeep.org/}bar')
+    custom_type.signature()
+    obj = custom_type(bar='foo')
+
+    node = etree.Element('document')
+    custom_type.render(node, obj)
+    expected = """
+        <document>
+            <ns0:bar xmlns:ns0="http://tests.python-zeep.org/">
+                <ns0:bar>foo</ns0:bar>
+            </ns0:bar>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_unqualified():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                attributeFormDefault="qualified"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="Address">
+            <complexType>
+              <sequence>
+                <element name="foo" type="xsd:string" form="unqualified" />
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+    obj = address_type(foo='bar')
+
+    expected = """
+      <document>
+        <ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
+          <foo>bar</foo>
+        </ns0:Address>
+      </document>
+    """
+
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_defaults():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <xsd:element name="container">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="foo" type="xsd:string" default="hoi"/>
+              </xsd:sequence>
+              <xsd:attribute name="bar" type="xsd:string" default="hoi"/>
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    container_type = schema.get_element(
+        '{http://tests.python-zeep.org/}container')
+    obj = container_type()
+    assert obj.foo == "hoi"
+    assert obj.bar == "hoi"
+
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/" bar="hoi">
+          <ns0:foo>hoi</ns0:foo>
+        </ns0:container>
+      </document>
+    """
+    node = etree.Element('document')
+    container_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+def test_defaults_parse():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <xsd:element name="container">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="foo" type="xsd:string" default="hoi" minOccurs="0"/>
+              </xsd:sequence>
+              <xsd:attribute name="bar" type="xsd:string" default="hoi"/>
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    container_elm = schema.get_element(
+        '{http://tests.python-zeep.org/}container')
+
+    node = load_xml("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:foo>hoi</ns0:foo>
+        </ns0:container>
+    """)
+    item = container_elm.parse(node, schema)
+    assert item.bar == 'hoi'
+
+
+def test_init_with_dicts():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <xsd:schema
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                attributeFormDefault="qualified"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <xsd:element name="Address">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="name" type="xsd:string"/>
+                <xsd:element minOccurs="0" name="optional" type="xsd:string"/>
+                <xsd:element name="container" nillable="true" type="tns:Container"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+
+          <xsd:complexType name="Container">
+            <xsd:sequence>
+              <xsd:element maxOccurs="unbounded" minOccurs="0" name="service"
+                           nillable="true" type="tns:ServiceRequestType"/>
+            </xsd:sequence>
+          </xsd:complexType>
+
+          <xsd:complexType name="ServiceRequestType">
+            <xsd:sequence>
+              <xsd:element name="name" type="xsd:string"/>
+            </xsd:sequence>
+          </xsd:complexType>
+        </xsd:schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+    address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
+    obj = address_type(name='foo', container={'service': [{'name': 'foo'}]})
+
+    expected = """
+      <document>
+        <ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:name>foo</ns0:name>
+          <ns0:container>
+            <ns0:service>
+              <ns0:name>foo</ns0:name>
+            </ns0:service>
+          </ns0:container>
+        </ns0:Address>
+      </document>
+    """
+
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    assert_nodes_equal(expected, node)
+
+
+
+def test_sequence_in_sequence():
+    node = load_xml("""
+        <?xml version="1.0"?>
+        <schema
+                xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <sequence>
+                  <element name="item_1" type="xsd:string"/>
+                  <element name="item_2" type="xsd:string"/>
+                </sequence>
+              </sequence>
+            </complexType>
+          </element>
+          <element name="foobar" type="xsd:string"/>
+        </schema>
+    """)
+    schema = xsd.Schema(node)
+    element = schema.get_element('ns0:container')
+    value = element(item_1="foo", item_2="bar")
+
+    node = etree.Element('document')
+    element.render(node, value)
+
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_sequence_in_sequence_many():
+    node = load_xml("""
+        <?xml version="1.0"?>
+        <schema
+                xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <sequence minOccurs="2" maxOccurs="2">
+                  <element name="item_1" type="xsd:string"/>
+                  <element name="item_2" type="xsd:string"/>
+                </sequence>
+              </sequence>
+            </complexType>
+          </element>
+          <element name="foobar" type="xsd:string"/>
+        </schema>
+    """)
+    schema = xsd.Schema(node)
+    element = schema.get_element('ns0:container')
+    value = element(_value_1=[
+        {'item_1': "value-1-1", 'item_2': "value-1-2"},
+        {'item_1': "value-2-1", 'item_2': "value-2-2"},
+    ])
+
+    assert value._value_1 == [
+        {'item_1': "value-1-1", 'item_2': "value-1-2"},
+        {'item_1': "value-2-1", 'item_2': "value-2-2"},
+    ]
+
+    node = etree.Element('document')
+    element.render(node, value)
+
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>value-1-1</ns0:item_1>
+          <ns0:item_2>value-1-2</ns0:item_2>
+          <ns0:item_1>value-2-1</ns0:item_1>
+          <ns0:item_2>value-2-2</ns0:item_2>
+        </ns0:container>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_complex_type_empty():
+    node = etree.fromstring("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <complexType name="empty"/>
+          <element name="container">
+            <complexType>
+              <sequence>
+                <element name="something" type="tns:empty"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+
+    container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
+    obj = container_elm()
+
+    node = etree.Element('document')
+    container_elm.render(node, obj)
+    expected = """
+        <document>
+            <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+                <ns0:something/>
+            </ns0:container>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+    item = container_elm.parse(node.getchildren()[0], schema)
+    assert item.something is None
+
+
+def test_schema_as_payload():
+    schema = xsd.Schema(load_xml("""
+        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <xsd:element name="container">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element ref="xsd:schema"/>
+                <xsd:any/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:schema>
+    """))
+    elm_class = schema.get_element('{http://tests.python-zeep.org/}container')
+
+    node = load_xml("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/"
+                       xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+          <xsd:schema
+              targetNamespace="http://tests.python-zeep.org/inline-schema"
+              elementFormDefault="qualified">
+            <xsd:element name="sub-element">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="item-1" type="xsd:string"/>
+                  <xsd:element name="item-2" type="xsd:string"/>
+                </xsd:sequence>
+              </xsd:complexType>
+            </xsd:element>
+          </xsd:schema>
+          <ns1:sub-element xmlns:ns1="http://tests.python-zeep.org/inline-schema">
+            <ns1:item-1>value-1</ns1:item-1>
+            <ns1:item-2>value-2</ns1:item-2>
+          </ns1:sub-element>
+        </ns0:container>
+    """)
+    value = elm_class.parse(node, schema)
+    assert value._value_1['item-1'] == 'value-1'
+    assert value._value_1['item-2'] == 'value-2'
+
+
+def test_nill():
+    schema = xsd.Schema(load_xml("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                elementFormDefault="qualified">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <element name="foo" type="string" nillable="true"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """))
+
+    address_type = schema.get_element('ns0:container')
+    obj = address_type()
+    expected = """
+      <document>
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
+        </ns0:container>
+      </document>
+    """
+    node = etree.Element('document')
+    address_type.render(node, obj)
+    etree.cleanup_namespaces(node)
+
+    assert_nodes_equal(expected, node)
+
+
+def test_empty_xmlns():
+    node = load_xml("""
+        <?xml version="1.0"?>
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/"
+                elementFormDefault="qualified">
+          <complexType name="empty"/>
+          <element name="container">
+            <complexType>
+              <sequence>
+                <element ref="schema"/>
+                <any/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """.strip())
+
+    schema = xsd.Schema(node)
+
+    container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
+    node = load_xml("""
+        <container>
+            <xs:schema
+                xmlns=""
+                xmlns:xs="http://www.w3.org/2001/XMLSchema"
+                xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="NewDataSet">
+              <xs:element name="something" type="xs:string" msdata:foo=""/>
+            </xs:schema>
+            <something>foo</something>
+        </container>
+    """)
+    item = container_elm.parse(node, schema)
+    assert item._value_1 == 'foo'
diff --git a/tests/test_xsd_parse.py b/tests/test_xsd_parse.py
new file mode 100644
index 0000000..c7f959e
--- /dev/null
+++ b/tests/test_xsd_parse.py
@@ -0,0 +1,859 @@
+import datetime
+from lxml import etree
+
+from tests.utils import load_xml
+from zeep import xsd
+from zeep.xsd.schema import Schema
+
+
+def test_sequence_parse_basic():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.String()),
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == 'bar'
+
+
+def test_sequence_parse_basic_with_attrs():
+    custom_element = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.String()),
+            ]),
+            [
+                xsd.Attribute(
+                    etree.QName('http://tests.python-zeep.org/', 'attr_1'),
+                    xsd.String()),
+                xsd.Attribute('attr_2', xsd.String()),
+            ]
+        ))
+    expected = etree.fromstring("""
+        <ns0:authentication xmlns:ns0="http://tests.python-zeep.org/" ns0:attr_1="x" attr_2="y">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:authentication>
+    """)
+    obj = custom_element.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == 'bar'
+    assert obj.attr_1 == 'x'
+    assert obj.attr_2 == 'y'
+
+
+def test_sequence_parse_with_optional():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'container'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.ComplexType(
+                        xsd.Sequence([
+                            xsd.Element(
+                                etree.QName('http://tests.python-zeep.org/', 'item_2_1'),
+                                xsd.String(),
+                                nillable=True)
+                        ])
+                    )
+                ),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_3'),
+                    xsd.String(),
+                    max_occurs=2),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_4'),
+                    xsd.String(),
+                    min_occurs=0),
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>1</ns0:item_1>
+          <ns0:item_2/>
+          <ns0:item_3>3</ns0:item_3>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == '1'
+    assert obj.item_2 is None
+    assert obj.item_3 == ['3']
+    assert obj.item_4 is None
+
+
+def test_sequence_parse_regression():
+    schema_doc = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <xsd:schema xmlns:tns="http://tests.python-zeep.org/attr"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          elementFormDefault="qualified"
+          targetNamespace="http://tests.python-zeep.org/attr">
+          <xsd:complexType name="Result">
+            <xsd:attribute name="id" type="xsd:int" use="required"/>
+          </xsd:complexType>
+          <xsd:element name="Response">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element minOccurs="0" maxOccurs="1" name="Result" type="tns:Result"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:schema>
+    """)
+
+    response_doc = load_xml(b"""
+        <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
+          <s:Body xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+            <Response xmlns="http://tests.python-zeep.org/attr">
+                <Result id="2"/>
+            </Response>
+          </s:Body>
+        </s:Envelope>
+    """)
+
+    schema = xsd.Schema(schema_doc)
+    elm = schema.get_element('{http://tests.python-zeep.org/attr}Response')
+
+    node = response_doc.xpath(
+        '//ns0:Response', namespaces={
+            'xsd': 'http://www.w3.org/2001/XMLSchema',
+            'ns0': 'http://tests.python-zeep.org/attr',
+        })
+    response = elm.parse(node[0], None)
+    assert response.Result.id == 2
+
+
+def test_sequence_parse_anytype():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'container'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.AnyType()),
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+
+
+def test_sequence_parse_anytype_nil():
+    schema = xsd.Schema(load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <xsd:schema xmlns:tns="http://tests.python-zeep.org/"
+          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+          elementFormDefault="qualified"
+          targetNamespace="http://tests.python-zeep.org/">
+          <xsd:element name="container">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element minOccurs="0" maxOccurs="1" name="item_1" type="xsd:string"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:schema>
+    """))
+
+    container = schema.get_element('{http://tests.python-zeep.org/}container')
+
+    expected = etree.fromstring("""
+        <ns0:container
+            xmlns:ns0="http://tests.python-zeep.org/"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+          <ns0:item_1 xsi:type="xsd:anyType"/>
+        </ns0:container>
+    """)
+    obj = container.parse(expected, schema)
+    assert obj.item_1 is None
+
+
+def test_sequence_parse_anytype_obj():
+    value_type = xsd.ComplexType(
+        xsd.Sequence([
+            xsd.Element(
+                '{http://tests.python-zeep.org/}value',
+                xsd.Integer()),
+        ])
+    )
+
+    schema = Schema(
+        etree.Element(
+            '{http://www.w3.org/2001/XMLSchema}Schema',
+            targetNamespace='http://tests.python-zeep.org/'))
+
+    root = list(schema._schemas.values())[0]
+    root.register_type('{http://tests.python-zeep.org/}something', value_type)
+
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'container'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.AnyType()),
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container
+            xmlns:ns0="http://tests.python-zeep.org/"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+          <ns0:item_1 xsi:type="ns0:something">
+            <ns0:value>100</ns0:value>
+          </ns0:item_1>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, schema)
+    assert obj.item_1.value == 100
+
+
+def test_sequence_parse_choice():
+    schema_doc = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <schema
+            xmlns="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/tst"
+            elementFormDefault="qualified"
+            targetNamespace="http://tests.python-zeep.org/tst">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <choice>
+                  <element name="item_1" type="xsd:string" />
+                  <element name="item_2" type="xsd:string" />
+                </choice>
+                <element name="item_3" type="xsd:string" />
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """)
+
+    xml = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <tst:container
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:tst="http://tests.python-zeep.org/tst">
+          <tst:item_1>blabla</tst:item_1>
+          <tst:item_3>haha</tst:item_3>
+        </tst:container>
+    """)
+
+    schema = xsd.Schema(schema_doc)
+    elm = schema.get_element('{http://tests.python-zeep.org/tst}container')
+    result = elm.parse(xml, schema)
+    assert result.item_1 == 'blabla'
+    assert result.item_3 == 'haha'
+
+
+def test_sequence_parse_choice_max_occurs():
+    schema_doc = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <schema
+            xmlns="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/tst"
+            elementFormDefault="qualified"
+            targetNamespace="http://tests.python-zeep.org/tst">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <choice maxOccurs="2">
+                  <element name="item_1" type="xsd:string" />
+                  <element name="item_2" type="xsd:string" />
+                </choice>
+                <element name="item_3" type="xsd:string" />
+              </sequence>
+              <attribute name="item_1" type="xsd:string" use="optional" />
+              <attribute name="item_2" type="xsd:string" use="optional" />
+            </complexType>
+          </element>
+        </schema>
+    """)
+
+    xml = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <tst:container
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:tst="http://tests.python-zeep.org/tst">
+          <tst:item_1>item-1-1</tst:item_1>
+          <tst:item_1>item-1-2</tst:item_1>
+          <tst:item_3>item-3</tst:item_3>
+        </tst:container>
+    """)
+
+    schema = xsd.Schema(schema_doc)
+    elm = schema.get_element('{http://tests.python-zeep.org/tst}container')
+    result = elm.parse(xml, schema)
+    assert result._value_1 == [
+        {'item_1': 'item-1-1'},
+        {'item_1': 'item-1-2'},
+    ]
+
+    assert result.item_3 == 'item-3'
+
+
+def test_sequence_parse_choice_sequence_max_occurs():
+    schema_doc = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <schema
+            xmlns="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/tst"
+            elementFormDefault="qualified"
+            targetNamespace="http://tests.python-zeep.org/tst">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <choice maxOccurs="3">
+                  <sequence>
+                    <element name="item_1" type="xsd:string" />
+                    <element name="item_2" type="xsd:string" />
+                  </sequence>
+                  <element name="item_3" type="xsd:string" />
+                </choice>
+                <element name="item_4" type="xsd:string" />
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """)
+
+    xml = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <tst:container
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:tst="http://tests.python-zeep.org/tst">
+          <tst:item_1>text-1</tst:item_1>
+          <tst:item_2>text-2</tst:item_2>
+          <tst:item_1>text-1</tst:item_1>
+          <tst:item_2>text-2</tst:item_2>
+          <tst:item_3>text-3</tst:item_3>
+          <tst:item_4>text-4</tst:item_4>
+        </tst:container>
+    """)
+
+    schema = xsd.Schema(schema_doc)
+    elm = schema.get_element('{http://tests.python-zeep.org/tst}container')
+    result = elm.parse(xml, schema)
+    assert result._value_1 == [
+        {'item_1': 'text-1', 'item_2': 'text-2'},
+        {'item_1': 'text-1', 'item_2': 'text-2'},
+        {'item_3': 'text-3'},
+    ]
+    assert result.item_4 == 'text-4'
+
+
+def test_sequence_parse_anytype_regression_17():
+    schema_doc = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <schema
+            xmlns="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/tst"
+            elementFormDefault="qualified"
+            targetNamespace="http://tests.python-zeep.org/tst">
+          <complexType name="CustomField">
+            <sequence>
+              <element name="parentItemURI" type="xsd:string"/>
+              <element name="key" type="xsd:string"/>
+              <element name="value" nillable="true"/>
+            </sequence>
+          </complexType>
+          <complexType name="Text">
+            <sequence>
+              <element name="type" type="xsd:string"/>
+              <element name="content" type="xsd:string"/>
+              <element name="contentLossy" type="xsd:boolean"/>
+            </sequence>
+          </complexType>
+
+          <element name="getCustomFieldResponse">
+            <complexType>
+              <sequence>
+                <element name="getCustomFieldReturn" type="tns:CustomField"/>
+              </sequence>
+            </complexType>
+          </element>
+        </schema>
+    """)
+
+    xml = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <tst:getCustomFieldResponse
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:tst="http://tests.python-zeep.org/tst">
+          <tst:getCustomFieldReturn>
+            <tst:parentItemURI>blabla</tst:parentItemURI>
+            <tst:key>solution</tst:key>
+            <tst:value xsi:type="tst:Text">
+              <tst:type xsi:type="xsd:string">text/html</tst:type>
+              <tst:content xsi:type="xsd:string">Test Solution</tst:content>
+              <tst:contentLossy xsi:type="xsd:boolean">false</tst:contentLossy>
+            </tst:value>
+          </tst:getCustomFieldReturn>
+        </tst:getCustomFieldResponse>
+    """)
+
+    schema = xsd.Schema(schema_doc)
+    elm = schema.get_element(
+        '{http://tests.python-zeep.org/tst}getCustomFieldResponse'
+    )
+    result = elm.parse(xml, schema)
+    assert result.getCustomFieldReturn.value.content == 'Test Solution'
+
+
+def test_sequence_min_occurs_2():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.String()),
+            ], min_occurs=2, max_occurs=2)
+        ))
+
+    # INIT
+    elm = custom_type(_value_1=[
+        {'item_1': 'foo-1', 'item_2': 'bar-1'},
+        {'item_1': 'foo-2', 'item_2': 'bar-2'},
+    ])
+
+    assert elm._value_1 == [
+        {'item_1': 'foo-1', 'item_2': 'bar-1'},
+        {'item_1': 'foo-2', 'item_2': 'bar-2'},
+    ]
+
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj._value_1 == [
+        {
+            'item_1': 'foo',
+            'item_2': 'bar',
+        },
+        {
+            'item_1': 'foo',
+            'item_2': 'bar',
+        },
+    ]
+
+
+def test_all_basic():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.All([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.String()),
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_2>bar</ns0:item_2>
+          <ns0:item_1>foo</ns0:item_1>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == 'bar'
+
+
+def test_group_optional():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Group(
+                etree.QName('http://tests.python-zeep.org/', 'foobar'),
+                xsd.Sequence([
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                        xsd.String()),
+                ]),
+                min_occurs=1)
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == 'bar'
+    assert not hasattr(obj, 'foobar')
+
+
+def test_group_min_occurs_2():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Group(
+                etree.QName('http://tests.python-zeep.org/', 'foobar'),
+                xsd.Sequence([
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                        xsd.String()),
+                ]),
+                min_occurs=2, max_occurs=2)
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj._value_1 == [
+        {'item_1': 'foo', 'item_2': 'bar'},
+        {'item_1': 'foo', 'item_2': 'bar'},
+    ]
+    assert not hasattr(obj, 'foobar')
+
+
+def test_group_min_occurs_2_sequence_min_occurs_2():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Group(
+                etree.QName('http://tests.python-zeep.org/', 'foobar'),
+                xsd.Sequence([
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                        xsd.String()),
+                    xsd.Element(
+                        etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                        xsd.String()),
+                ], min_occurs=2, max_occurs=2),
+                min_occurs=2, max_occurs=2)
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj._value_1 == [
+        {'_value_1': [
+            {'item_1': 'foo', 'item_2': 'bar'},
+            {'item_1': 'foo', 'item_2': 'bar'},
+        ]},
+        {'_value_1': [
+            {'item_1': 'foo', 'item_2': 'bar'},
+            {'item_1': 'foo', 'item_2': 'bar'},
+        ]},
+    ]
+    assert not hasattr(obj, 'foobar')
+
+
+def test_nested_complex_type():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.ComplexType(
+                        xsd.Sequence([
+                            xsd.Element(
+                                '{http://tests.python-zeep.org/}item_2a',
+                                xsd.String()),
+                            xsd.Element(
+                                '{http://tests.python-zeep.org/}item_2b',
+                                xsd.String()),
+                        ])
+                    )
+                )
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>
+            <ns0:item_2a>2a</ns0:item_2a>
+            <ns0:item_2b>2b</ns0:item_2b>
+          </ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2.item_2a == '2a'
+    assert obj.item_2.item_2b == '2b'
+
+
+def test_nested_complex_type_optional():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_2'),
+                    xsd.ComplexType(
+                        xsd.Sequence([
+                            xsd.Choice([
+                                xsd.Element(
+                                    '{http://tests.python-zeep.org/}item_2a1',
+                                    xsd.String(),
+                                    min_occurs=0),
+                                xsd.Element(
+                                    '{http://tests.python-zeep.org/}item_2a2',
+                                    xsd.String(),
+                                    min_occurs=0),
+                            ]),
+                            xsd.Element(
+                                '{http://tests.python-zeep.org/}item_2b',
+                                xsd.String()),
+                        ])
+                    ),
+                    min_occurs=0, max_occurs='unbounded'
+                )
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == []
+
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2/>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == []
+
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>
+            <ns0:item_2a1>x</ns0:item_2a1>
+          </ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2[0].item_2a1 == 'x'
+    assert obj.item_2[0].item_2b is None
+
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>
+            <ns0:item_2a1>x</ns0:item_2a1>
+            <ns0:item_2b/>
+          </ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2[0].item_2a1 == 'x'
+    assert obj.item_2[0].item_2b is None
+
+
+def test_nested_choice_optional():
+    custom_type = xsd.Element(
+        etree.QName('http://tests.python-zeep.org/', 'authentication'),
+        xsd.ComplexType(
+            xsd.Sequence([
+                xsd.Element(
+                    etree.QName('http://tests.python-zeep.org/', 'item_1'),
+                    xsd.String()),
+                xsd.Choice([
+                        xsd.Element(
+                            '{http://tests.python-zeep.org/}item_2',
+                            xsd.String()),
+                        xsd.Element(
+                            '{http://tests.python-zeep.org/}item_3',
+                            xsd.String()),
+                    ],
+                    min_occurs=0, max_occurs=1
+                ),
+            ])
+        ))
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+          <ns0:item_2>bar</ns0:item_2>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 == 'bar'
+
+    expected = etree.fromstring("""
+        <ns0:container xmlns:ns0="http://tests.python-zeep.org/">
+          <ns0:item_1>foo</ns0:item_1>
+        </ns0:container>
+    """)
+    obj = custom_type.parse(expected, None)
+    assert obj.item_1 == 'foo'
+    assert obj.item_2 is None
+    assert obj.item_3 is None
+
+
+def test_union():
+    schema_doc = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <xsd:schema
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/tst"
+            elementFormDefault="qualified"
+            targetNamespace="http://tests.python-zeep.org/tst">
+
+          <xsd:element name="State" type="tns:StateType"/>
+
+          <xsd:complexType name="StateType">
+            <xsd:simpleContent>
+                <xsd:extension base="tns:StateBaseType">
+                  <xsd:anyAttribute namespace="##other" processContents="lax"/>
+                </xsd:extension>
+            </xsd:simpleContent>
+          </xsd:complexType>
+          <xsd:simpleType name="tns:StateBaseType">
+            <xsd:union memberTypes="tns:Type1 tns:Type2"/>
+          </xsd:simpleType>
+
+          <xsd:simpleType name="Type1">
+            <xsd:restriction base="xsd:NMTOKEN">
+              <xsd:maxLength value="255"/>
+              <xsd:enumeration value="Idle"/>
+              <xsd:enumeration value="Processing"/>
+              <xsd:enumeration value="Stopped"/>
+            </xsd:restriction>
+          </xsd:simpleType>
+
+          <xsd:simpleType name="Type2">
+            <xsd:restriction base="xsd:NMTOKEN">
+              <xsd:maxLength value="255"/>
+              <xsd:enumeration value="Paused"/>
+            </xsd:restriction>
+          </xsd:simpleType>
+        </xsd:schema>
+    """)
+
+    xml = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <tst:State xmlns:tst="http://tests.python-zeep.org/tst">Idle</tst:State>
+    """)
+
+    schema = xsd.Schema(schema_doc)
+    elm = schema.get_element('{http://tests.python-zeep.org/tst}State')
+    result = elm.parse(xml, schema)
+    assert result._value_1 == 'Idle'
+
+
+def test_parse_invalid_values():
+    schema = xsd.Schema(load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <schema
+            xmlns="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:tns="http://tests.python-zeep.org/"
+            elementFormDefault="qualified"
+            targetNamespace="http://tests.python-zeep.org/">
+          <element name="container">
+            <complexType>
+              <sequence>
+                <element name="item_1" type="xsd:dateTime" />
+                <element name="item_2" type="xsd:date" />
+              </sequence>
+              <attribute name="attr_1" type="xsd:dateTime" />
+              <attribute name="attr_2" type="xsd:date" />
+            </complexType>
+          </element>
+        </schema>
+    """))
+
+    xml = load_xml(b"""
+        <?xml version="1.0" encoding="utf-8"?>
+        <tns:container
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xmlns:tns="http://tests.python-zeep.org/"
+            attr_1="::" attr_2="2013-10-20">
+          <tns:item_1>foo</tns:item_1>
+          <tns:item_2>2016-10-20</tns:item_2>
+        </tns:container>
+    """)
+
+    elm = schema.get_element('{http://tests.python-zeep.org/}container')
+    result = elm.parse(xml, schema)
+    assert result.item_1 is None
+    assert result.item_2 == datetime.date(2016, 10, 20)
+    assert result.attr_1 is None
+    assert result.attr_2 == datetime.date(2013, 10, 20)
diff --git a/tests/test_xsd_visitor.py b/tests/test_xsd_visitor.py
new file mode 100644
index 0000000..e731e08
--- /dev/null
+++ b/tests/test_xsd_visitor.py
@@ -0,0 +1,579 @@
+from lxml import etree
+
+from tests.utils import assert_nodes_equal, load_xml, render_node
+from zeep import xsd
+from zeep.xsd import builtins
+from zeep.xsd.context import ParserContext
+from zeep.xsd.schema import Schema
+
+
+def parse_schema_node(node):
+    parser_context = ParserContext()
+    schema = Schema(
+        node=node,
+        transport=None,
+        location=None,
+        parser_context=parser_context)
+    return schema
+
+
+def test_schema_empty():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+              targetNamespace="http://tests.python-zeep.org/"
+              elementFormDefault="qualified"
+              attributeFormDefault="unqualified">
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    root = list(schema._schemas.values())[0]
+    assert root._element_form == 'qualified'
+    assert root._attribute_form == 'unqualified'
+
+
+def test_element_simle_types():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+              targetNamespace="http://tests.python-zeep.org/">
+            <element name="foo" type="string" />
+            <element name="bar" type="int" />
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    assert schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert schema.get_element('{http://tests.python-zeep.org/}bar')
+
+
+def test_element_simple_type_annotation():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+              targetNamespace="http://tests.python-zeep.org/">
+            <element name="foo" type="string">
+                <annotation>
+                    <documentation>HOI!</documentation>
+                </annotation>
+            </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert element
+
+
+def test_element_default_type():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+            <element name="foo" />
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert isinstance(element.type, builtins.AnyType)
+
+
+def test_element_simple_type_unresolved():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+            <element name="foo" type="tns:unresolved">
+                <annotation>
+                    <documentation>HOI!</documentation>
+                </annotation>
+            </element>
+            <simpleType name="unresolved">
+                <restriction base="integer">
+                    <minInclusive value="0"/>
+                    <maxInclusive value="100"/>
+                </restriction>
+            </simpleType>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    assert schema.get_type('{http://tests.python-zeep.org/}unresolved')
+
+
+def test_element_max_occurs():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+              targetNamespace="http://tests.python-zeep.org/">
+            <element name="container">
+                <complexType>
+                    <sequence>
+                        <element name="e1" type="string" />
+                        <element name="e2" type="string" maxOccurs="1" />
+                        <element name="e3" type="string" maxOccurs="2" />
+                        <element name="e4" type="string" maxOccurs="unbounded" />
+                    </sequence>
+                </complexType>
+            </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    elm = schema.get_element('{http://tests.python-zeep.org/}container')
+    elements = dict(elm.type.elements)
+
+    assert isinstance(elements['e1'], xsd.Element)
+    assert elements['e1'].max_occurs == 1
+    assert isinstance(elements['e2'], xsd.Element)
+    assert elements['e2'].max_occurs == 1
+    assert isinstance(elements['e3'], xsd.Element)
+    assert elements['e3'].max_occurs == 2
+    assert isinstance(elements['e4'], xsd.Element)
+    assert elements['e4'].max_occurs == 'unbounded'
+
+
+def test_simple_content():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                targetNamespace="http://tests.python-zeep.org/">
+            <complexType name="container">
+                <simpleContent>
+                    <extension base="xsd:string">
+                        <attribute name="sizing" type="xsd:string" />
+                    </extension>
+                </simpleContent>
+            </complexType>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_type = schema.get_type('{http://tests.python-zeep.org/}container')
+    assert xsd_type(10, sizing='qwe')
+
+
+def test_attribute_optional():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="foo">
+            <complexType>
+              <xsd:attribute name="base" type="xsd:string" />
+            </complexType>
+          </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    value = xsd_element()
+
+    node = render_node(xsd_element, value)
+    expected = """
+      <document>
+        <ns0:foo xmlns:ns0="http://tests.python-zeep.org/"/>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_attribute_required():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="foo">
+            <complexType>
+              <xsd:attribute name="base" use="required" type="xsd:string" />
+            </complexType>
+          </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    value = xsd_element()
+
+    node = render_node(xsd_element, value)
+    expected = """
+      <document>
+        <ns0:foo xmlns:ns0="http://tests.python-zeep.org/" base=""/>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_attribute_default():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="foo">
+            <complexType>
+              <xsd:attribute name="base" default="x" type="xsd:string" />
+            </complexType>
+          </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    value = xsd_element()
+
+    node = render_node(xsd_element, value)
+    expected = """
+      <document>
+        <ns0:foo xmlns:ns0="http://tests.python-zeep.org/" base="x"/>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_attribute_simple_type():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                targetNamespace="http://tests.python-zeep.org/">
+            <element name="foo">
+              <complexType>
+                <attribute name="bar" use="optional">
+                 <simpleType>
+                  <restriction base="string">
+                   <enumeration value="hoi"/>
+                   <enumeration value="doei"/>
+                  </restriction>
+                 </simpleType>
+                </attribute>
+            </complexType>
+          </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert xsd_element(bar='hoi')
+
+
+def test_attribute_any_type():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                targetNamespace="http://tests.python-zeep.org/">
+          <element name="foo">
+            <complexType>
+              <xsd:attribute name="base" type="xsd:anyURI" />
+            </complexType>
+          </element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    value = xsd_element(base='hoi')
+
+    node = render_node(xsd_element, value)
+    expected = """
+      <document>
+        <ns0:foo xmlns:ns0="http://tests.python-zeep.org/" base="hoi"/>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_complex_content_mixed():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+          <xsd:element name="foo">
+            <xsd:complexType>
+              <xsd:complexContent mixed="true">
+                <xsd:extension base="xsd:anyType">
+                  <xsd:attribute name="bar" type="xsd:anyURI" use="required"/>
+                </xsd:extension>
+              </xsd:complexContent>
+            </xsd:complexType>
+          </xsd:element>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    result = xsd_element('basetype', bar='hoi')
+
+    node = etree.Element('document')
+    xsd_element.render(node, result)
+
+    expected = """
+      <document>
+        <ns0:foo xmlns:ns0="http://tests.python-zeep.org/" bar="hoi">basetype</ns0:foo>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_complex_content_extension():
+    node = load_xml("""
+        <schema
+                xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <complexType name="BaseType" abstract="true">
+            <sequence>
+              <element name="name" type="xsd:string" minOccurs="0"/>
+            </sequence>
+          </complexType>
+          <complexType name="SubType1">
+            <complexContent>
+              <extension base="tns:BaseType">
+                <attribute name="attr_1" type="xsd:string"/>
+                <attribute name="attr_2" type="xsd:string"/>
+              </extension>
+            </complexContent>
+          </complexType>
+          <complexType name="SubType2">
+            <complexContent>
+              <extension base="tns:BaseType">
+                <attribute name="attr_a" type="xsd:string"/>
+                <attribute name="attr_b" type="xsd:string"/>
+                <attribute name="attr_c" type="xsd:string"/>
+              </extension>
+            </complexContent>
+          </complexType>
+          <element name="test" type="tns:BaseType"/>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+
+    record_type = schema.get_type('{http://tests.python-zeep.org/}SubType1')
+    assert len(record_type.attributes) == 2
+    assert len(record_type.elements) == 1
+
+    record_type = schema.get_type('{http://tests.python-zeep.org/}SubType2')
+    assert len(record_type.attributes) == 3
+    assert len(record_type.elements) == 1
+
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}test')
+    xsd_type = schema.get_type('{http://tests.python-zeep.org/}SubType2')
+
+    value = xsd_type(attr_a='a', attr_b='b', attr_c='c')
+    node = render_node(xsd_element, value)
+    expected = """
+      <document>
+        <ns0:test
+            xmlns:ns0="http://tests.python-zeep.org/"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            attr_a="a" attr_b="b" attr_c="c" xsi:type="ns0:SubType2"/>
+      </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_simple_content_extension():
+    node = load_xml("""
+        <schema
+                xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                elementFormDefault="qualified"
+                targetNamespace="http://tests.python-zeep.org/">
+          <simpleType name="BaseType">
+            <restriction base="xsd:integer">
+              <minInclusive value="0"/>
+              <maxInclusive value="100"/>
+            </restriction>
+          </simpleType>
+          <complexType name="SubType1">
+            <simpleContent>
+              <extension base="tns:BaseType">
+                <attribute name="attr_1" type="xsd:string"/>
+                <attribute name="attr_2" type="xsd:string"/>
+              </extension>
+            </simpleContent>
+          </complexType>
+          <complexType name="SubType2">
+            <simpleContent>
+              <extension base="tns:BaseType">
+                <attribute name="attr_a" type="xsd:string"/>
+                <attribute name="attr_b" type="xsd:string"/>
+                <attribute name="attr_c" type="xsd:string"/>
+              </extension>
+            </simpleContent>
+          </complexType>
+        </schema>
+    """)
+    schema = parse_schema_node(node)
+
+    record_type = schema.get_type('{http://tests.python-zeep.org/}SubType1')
+    assert len(record_type.attributes) == 2
+    assert len(record_type.elements) == 1
+
+    record_type = schema.get_type('{http://tests.python-zeep.org/}SubType2')
+    assert len(record_type.attributes) == 3
+    assert len(record_type.elements) == 1
+
+
+def test_list_type():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+
+          <xsd:simpleType name="listOfIntegers">
+            <xsd:list itemType="integer" />
+          </xsd:simpleType>
+
+          <xsd:element name="foo">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="arg" type="tns:listOfIntegers"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </schema>
+    """)
+
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element(
+        '{http://tests.python-zeep.org/}foo')
+    value = xsd_element(arg=[1, 2, 3, 4, 5])
+
+    node = render_node(xsd_element, value)
+    expected = """
+        <document>
+          <ns0:foo xmlns:ns0="http://tests.python-zeep.org/">
+            <arg>1 2 3 4 5</arg>
+          </ns0:foo>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_list_type_unresolved():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+
+          <xsd:simpleType name="listOfIntegers">
+            <xsd:list itemType="tns:something" />
+          </xsd:simpleType>
+
+          <xsd:simpleType name="something">
+            <xsd:restriction base="xsd:integer" />
+          </xsd:simpleType>
+
+          <xsd:element name="foo">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="arg" type="tns:listOfIntegers"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </schema>
+    """)
+
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element(
+        '{http://tests.python-zeep.org/}foo')
+    value = xsd_element(arg=[1, 2, 3, 4, 5])
+
+    node = render_node(xsd_element, value)
+    expected = """
+        <document>
+          <ns0:foo xmlns:ns0="http://tests.python-zeep.org/">
+            <arg>1 2 3 4 5</arg>
+          </ns0:foo>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_list_type_simple_type():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+
+          <xsd:simpleType name="listOfIntegers">
+            <xsd:list>
+              <simpleType>
+                <xsd:restriction base="xsd:integer" />
+              </simpleType>
+            </xsd:list>
+          </xsd:simpleType>
+
+          <xsd:element name="foo">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="arg" type="tns:listOfIntegers"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </schema>
+    """)
+
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element(
+        '{http://tests.python-zeep.org/}foo')
+    value = xsd_element(arg=[1, 2, 3, 4, 5])
+
+    node = render_node(xsd_element, value)
+    expected = """
+        <document>
+          <ns0:foo xmlns:ns0="http://tests.python-zeep.org/">
+            <arg>1 2 3 4 5</arg>
+          </ns0:foo>
+        </document>
+    """
+    assert_nodes_equal(expected, node)
+
+
+def test_union_type():
+    node = load_xml("""
+        <schema xmlns="http://www.w3.org/2001/XMLSchema"
+                xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+                xmlns:tns="http://tests.python-zeep.org/"
+                targetNamespace="http://tests.python-zeep.org/">
+          <xsd:simpleType name="type">
+            <xsd:union memberTypes="xsd:language">
+              <xsd:simpleType>
+                <xsd:restriction base="xsd:string">
+                  <xsd:enumeration value=""/>
+                </xsd:restriction>
+              </xsd:simpleType>
+            </xsd:union>
+          </xsd:simpleType>
+
+          <xsd:element name="foo">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="arg" type="tns:type"/>
+              </xsd:sequence>
+            </xsd:complexType>
+          </xsd:element>
+        </schema>
+    """)
+
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_element('{http://tests.python-zeep.org/}foo')
+    assert xsd_element(arg='hoi')
+
+
+def test_simple_type_restriction():
+    node = load_xml("""
+        <xsd:schema
+            xmlns="http://tests.python-zeep.org/"
+            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+            targetNamespace="http://tests.python-zeep.org/"
+            elementFormDefault="qualified"
+            attributeFormDefault="unqualified">
+          <xsd:simpleType name="type_3">
+            <xsd:restriction base="type_2"/>
+          </xsd:simpleType>
+          <xsd:simpleType name="type_2">
+            <xsd:restriction base="type_1"/>
+          </xsd:simpleType>
+          <xsd:simpleType name="type_1">
+            <xsd:restriction base="xsd:int">
+              <xsd:totalDigits value="3"/>
+            </xsd:restriction>
+          </xsd:simpleType>
+        </xsd:schema>
+    """)
+    schema = parse_schema_node(node)
+    xsd_element = schema.get_type('{http://tests.python-zeep.org/}type_3')
+    assert xsd_element(100) == '100'
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..b0e8a16
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,44 @@
+import six
+from lxml import etree
+from six import binary_type, string_types
+
+
+def load_xml(xml):
+    parser = etree.XMLParser(remove_blank_text=True, remove_comments=True)
+    return etree.fromstring(xml.strip(), parser=parser)
+
+
+def assert_nodes_equal(result, expected):
+    def _convert_node(node):
+        if isinstance(node, (string_types, binary_type)):
+            return load_xml(node)
+        return node
+
+    # assert node_1 == node_2
+    result = etree.tostring(_convert_node(result), pretty_print=True)
+    expected = etree.tostring(_convert_node(expected), pretty_print=True)
+
+    if six.PY3:
+        result = result.decode('utf-8')
+        expected = expected.decode('utf-8')
+    assert result == expected
+
+
+def render_node(element, value):
+    node = etree.Element('document')
+    element.render(node, value)
+    return node
+
+
+class DummyTransport(object):
+    def __init__(self):
+        self._items = {}
+
+    def bind(self, url, node):
+        self._items[url] = node
+
+    def load(self, url):
+        data = self._items[url]
+        if isinstance(data, string_types):
+            return data
+        return etree.tostring(data)
diff --git a/tests/wsdl_files/http.wsdl b/tests/wsdl_files/http.wsdl
new file mode 100644
index 0000000..5622ff1
--- /dev/null
+++ b/tests/wsdl_files/http.wsdl
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<definitions 
+    xmlns:tns="http://example.com/stockquote.wsdl" 
+    xmlns:xsd1="http://example.com/stockquote.xsd" 
+    xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" 
+    xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" 
+    xmlns="http://schemas.xmlsoap.org/wsdl/"
+    name="StockQuote" 
+    targetNamespace="http://example.com/stockquote.wsdl">
+  <types>
+    <schema xmlns="http://www.w3.org/2001/XMLSchema" 
+            targetNamespace="http://example.com/stockquote.xsd">
+      <complexType name="Address">
+        <sequence>
+          <element minOccurs="0" maxOccurs="1" name="NameFirst" type="string"/>
+          <element minOccurs="0" maxOccurs="1" name="NameLast" type="string"/>
+          <element minOccurs="0" maxOccurs="1" name="Email" type="string"/>
+        </sequence>
+      </complexType>
+      <element name="TradePriceRequest">
+        <complexType>
+          <all>
+            <element name="tickerSymbol" type="string"/>
+          </all>
+        </complexType>
+      </element>
+      <element name="TradePrice">
+        <complexType>
+          <all>
+            <element name="price" type="float"/>
+          </all>
+        </complexType>
+      </element>
+    </schema>
+  </types>
+  <message name="GetLastTradePriceInput">
+    <part name="body" element="xsd1:TradePriceRequest"/>
+  </message>
+  <message name="GetLastTradePriceOutput">
+    <part name="body" element="xsd1:TradePrice"/>
+  </message>
+  <portType name="StockQuotePortType">
+    <operation name="GetLastTradePrice">
+      <input message="tns:GetLastTradePriceInput"/>
+      <output message="tns:GetLastTradePriceOutput"/>
+    </operation>
+  </portType>
+  <binding name="StockQuoteBinding" type="tns:StockQuotePortType">
+    <http:binding verb="POST"/>
+    <operation name="GetLastTradePrice">
+      <http:operation location="GetLastTradePrice"/>
+      <input>
+        <mime:content type="application/x-www-form-urlencoded"/>
+      </input>
+      <output>
+        <mime:mimeXml/>
+      </output>
+    </operation>
+  </binding>
+  <service name="StockQuoteService">
+    <documentation>My first service</documentation>
+    <port name="StockQuotePort" binding="tns:StockQuoteBinding">
+      <http:address location="http://example.com/stockquote"/>
+    </port>
+  </service>
+</definitions>
diff --git a/tests/wsdl_files/soap.wsdl b/tests/wsdl_files/soap.wsdl
new file mode 100644
index 0000000..6cc923c
--- /dev/null
+++ b/tests/wsdl_files/soap.wsdl
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<definitions 
+    xmlns:tns="http://example.com/stockquote.wsdl" 
+    xmlns:xsd1="http://example.com/stockquote.xsd" 
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
+    xmlns="http://schemas.xmlsoap.org/wsdl/"
+    name="StockQuote" 
+    targetNamespace="http://example.com/stockquote.wsdl">
+  <types>
+    <schema xmlns="http://www.w3.org/2001/XMLSchema" 
+            targetNamespace="http://example.com/stockquote.xsd"
+            xmlns:tns="http://example.com/stockquote.xsd" >
+      <complexType name="Address">
+        <sequence>
+          <element minOccurs="0" maxOccurs="1" name="NameFirst" type="string"/>
+          <element minOccurs="0" maxOccurs="1" name="NameLast" type="string"/>
+          <element minOccurs="0" maxOccurs="1" name="Email" type="string"/>
+        </sequence>
+      </complexType>
+      <element name="Fault1">
+        <complexType>
+          <sequence>
+            <element name="message" type="string"/>
+          </sequence>
+        </complexType>
+      </element>
+      <element name="Fault2">
+        <complexType>
+          <sequence>
+            <element name="message" type="string"/>
+          </sequence>
+        </complexType>
+      </element>
+      <element name="TradePriceRequest">
+        <complexType>
+          <all>
+            <element name="tickerSymbol" type="string"/>
+            <element name="account" type="tns:account" minOccurs="0" />
+            <element ref="tns:country"/>
+          </all>
+        </complexType>
+      </element>
+      <element name="TradePrice">
+        <complexType>
+          <all>
+            <element name="price" type="float"/>
+          </all>
+        </complexType>
+      </element>
+      <complexType name="account">
+        <sequence>
+          <element name="id" type="int"/>
+          <element name="user" type="string"/>
+        </sequence>
+      </complexType>
+      <complexType name="country">
+          <sequence>
+            <element name="code" type="string"/>
+          </sequence>
+      </complexType>
+      <element name="country">
+        <complexType>
+          <sequence>
+            <element name="name" type="string"/>
+            <element name="code" type="string"/>
+          </sequence>
+        </complexType>
+      </element>
+    </schema>
+  </types>
+  <message name="GetLastTradePriceInput">
+    <part name="body" element="xsd1:TradePriceRequest"/>
+  </message>
+  <message name="GetLastTradePriceOutput">
+    <part name="body" element="xsd1:TradePrice"/>
+  </message>
+  <message name="FaultMessageMsg1">
+    <part name="fault1" element="xsd1:Fault1"/>
+  </message>
+  <message name="FaultMessageMsg2">
+    <part name="fault2" element="xsd1:Fault2"/>
+  </message>
+  <portType name="StockQuotePortType">
+    <operation name="GetLastTradePrice">
+      <input message="tns:GetLastTradePriceInput"/>
+      <output message="tns:GetLastTradePriceOutput"/>
+      <fault message="tns:FaultMessageMsg1" name="fault1"/>
+      <fault message="tns:FaultMessageMsg2" name="fault2"/>
+    </operation>
+  </portType>
+  <binding name="StockQuoteBinding" type="tns:StockQuotePortType">
+    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+    <operation name="GetLastTradePrice">
+      <soap:operation soapAction="http://example.com/GetLastTradePrice"/>
+      <input>
+        <soap:body use="literal"/>
+      </input>
+      <output>
+        <soap:body use="literal"/>
+      </output>
+      <fault name="fault1">
+        <soap:fault name="fault1" use="literal"/>
+      </fault>
+      <fault name="fault2">
+        <soap:fault name="fault2" use="literal"/>
+      </fault>
+    </operation>
+  </binding>
+  <service name="StockQuoteService">
+    <documentation>My first service</documentation>
+    <port name="StockQuotePort" binding="tns:StockQuoteBinding">
+      <soap:address location="http://example.com/stockquote"/>
+    </port>
+  </service>
+</definitions>
diff --git a/tests/wsdl_files/soap_header.wsdl b/tests/wsdl_files/soap_header.wsdl
new file mode 100644
index 0000000..b3f6c6b
--- /dev/null
+++ b/tests/wsdl_files/soap_header.wsdl
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<definitions xmlns:tns="http://example.com/stockquote.wsdl" xmlns:xsd1="http://example.com/stockquote.xsd" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns="http://schemas.xmlsoap.org/wsdl/" name="StockQuote" targetNamespace="http://example.com/stockquote.wsdl">
+  <types>
+    <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/stockquote.xsd">
+      <complexType name="Address">
+        <sequence>
+          <element minOccurs="0" maxOccurs="1" name="NameFirst" type="string"/>
+          <element minOccurs="0" maxOccurs="1" name="NameLast" type="string"/>
+          <element minOccurs="0" maxOccurs="1" name="Email" type="string"/>
+        </sequence>
+      </complexType>
+      <element name="TradePriceRequest">
+        <complexType>
+          <all>
+            <element name="tickerSymbol" type="string"/>
+          </all>
+        </complexType>
+      </element>
+      <element name="TradePrice">
+        <complexType>
+          <all>
+            <element name="price" type="float"/>
+          </all>
+        </complexType>
+      </element>
+      <element name="Authentication">
+        <complexType>
+          <sequence>
+            <element name="username" type="string"/>
+            <element name="password" type="string"/>
+          </sequence>
+        </complexType>
+      </element>
+    </schema>
+  </types>
+  <message name="GetLastTradePriceInput">
+    <part name="header" element="xsd1:Authentication"/>
+    <part name="body" element="xsd1:TradePriceRequest"/>
+  </message>
+  <message name="GetLastTradePriceOutput">
+    <part name="body" element="xsd1:TradePrice"/>
+  </message>
+  <portType name="StockQuotePortType">
+    <operation name="GetLastTradePrice">
+      <input message="tns:GetLastTradePriceInput"/>
+      <output message="tns:GetLastTradePriceOutput"/>
+    </operation>
+  </portType>
+  <binding name="StockQuoteBinding" type="tns:StockQuotePortType">
+    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+    <operation name="GetLastTradePrice">
+      <soap:operation soapAction="http://example.com/GetLastTradePrice"/>
+      <input>
+        <soap:header message="tns:GetLastTradePriceInput" part="header" use="literal"/>
+        <soap:body use="literal"/>
+      </input>
+      <output>
+        <soap:body use="literal"/>
+      </output>
+    </operation>
+  </binding>
+  <service name="StockQuoteService">
+    <documentation>My first service</documentation>
+    <port name="StockQuotePort" binding="tns:StockQuoteBinding">
+      <soap:address location="http://example.com/stockquote"/>
+    </port>
+  </service>
+</definitions>
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..be542d5
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,7 @@
+[tox]
+envlist = py27,py33,py34,py35,pypy
+
+[testenv]
+commands = 
+    pip install .[test]
+    py.test -vvv
-- 
python-zeep



More information about the tryton-debian-vcs mailing list