[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