Adding debian version 0.23.0-1.

This commit is contained in:
Mathias Behrle 2016-12-06 17:32:03 +01:00
commit 90d4bf5939
101 changed files with 15716 additions and 0 deletions

19
.editorconfig Normal file
View File

@ -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

20
.gitignore vendored Normal file
View File

@ -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/

40
.travis.yml Normal file
View File

@ -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

355
CHANGES Normal file
View File

@ -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 :)

53
LICENSE Normal file
View File

@ -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.

30
Makefile Normal file
View File

@ -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/*

87
PKG-INFO Normal file
View File

@ -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@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@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

90
README.rst Normal file
View File

@ -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@mvantellingen.nl for more information.
.. _let me know: https://github.com/mvantellingen/python-zeep/issues

5
debian/changelog vendored Normal file
View File

@ -0,0 +1,5 @@
python-zeep (0.23.0-1) unstable; urgency=medium
* Initial commit (Closes: #834485).
-- Mathias Behrle <mbehrle@debian.org> Tue, 06 Dec 2016 19:07:51 +0100

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

126
debian/control vendored Normal file
View File

@ -0,0 +1,126 @@
Source: python-zeep
Section: python
Priority: optional
Maintainer: Debian Tryton Maintainers <maintainers@debian.tryton.org>
Uploaders:
Mathias Behrle <mbehrle@debian.org>,
Build-Depends:
debhelper (>= 9),
dh-python (>= 1.20130901-1~),
python,
python-appdirs,
python-cached-property,
python-defusedxml,
python-flake8,
python-freezegun,
python-isodate,
python-isort,
python-lxml,
python-mock,
python-pretend,
python-pytest,
python-pytest-cov,
python-requests,
python-requests-mock,
python-setuptools,
python-six,
python-tz,
python3,
python3-appdirs,
python3-cached-property,
python3-defusedxml,
python3-flake8,
python3-freezegun,
python3-isodate,
python3-isort,
python3-lxml,
python3-mock,
python3-pretend,
python3-pytest,
python3-pytest-cov,
python3-requests,
python3-requests-mock,
python3-setuptools,
python3-six,
python3-tz,
Standards-Version: 3.9.8
Homepage: https://github.com/mvantellingen/python-zeep
Vcs-Browser: https://anonscm.debian.org/cgit/tryton/python-zeep.git
Vcs-Git: https://anonscm.debian.org/cgit/tryton/python-zeep.git
Package: python-zeep
Architecture: all
Depends:
python-appdirs,
python-cached-property,
python-defusedxml,
python-freezegun,
python-isodate,
python-lxml,
python-pkg-resources,
python-requests,
python-setuptools,
python-six,
python-tz,
${misc:Depends},
${python:Depends},
Description: Modern SOAP client library (Python 2)
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
.
Please see for more information the documentation at
http://docs.python-zeep.org/
.
This package is targeting Python version 2.
Package: python3-zeep
Architecture: all
Depends:
python3-appdirs,
python3-cached-property,
python3-defusedxml,
python3-freezegun,
python3-isodate,
python3-lxml,
python3-pkg-resources,
python3-requests,
python3-six,
python3-tz,
${misc:Depends},
${python3:Depends},
Description: Modern SOAP client library (Python 3)
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
.
Please see for more information the documentation at
http://docs.python-zeep.org/
.
This package is targeting Python version 3.

125
debian/copyright vendored Normal file
View File

@ -0,0 +1,125 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files: *
Copyright: 2016 Michael van Tellingen
License: Expat
Files: src/zeep/xsd/*
Copyright: 2011-2014 soapfish contributors
2016 Michael van Tellingen
License: BSD-3-clause or Expat
Comments:
Parts of the XSD handling are heavily inspired by soapfish, see:
https://github.com/FlightDataServices/soapfish
Files: tests/integration/hello_world_recursive.wdsl
tests/integration/hello_world_recursive_import.wdsl
Copyright: not applicable
License: Apache
Files: tests/wsdl_files/soap-enc.xsd
tests/wsdl_files/xmldsig-core-schema.xsd
Copyright: 2001 DevelopMentor
2001 W3C (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University)
License: W3C
Comments:
Original W3C files; http://www.w3.org/2001/06/soap-encoding
Files: debian/*
Copyright: 2016 Mathias Behrle <mbehrle@debian.org>
License: Expat
License: Expat
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.
License: BSD-3-clause
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.
License: Apache
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.
License: W3C
Portions © 2001 DevelopMentor.
© 2001 W3C (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved.
.
This document is governed by the W3C Software License [1] as described in the FAQ [2].
[1] http://www.w3.org/Consortium/Legal/copyright-software-19980720
[2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD
By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
.
Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
.
1. The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
.
2. Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, a short notice of the following form (hypertext is preferred, text is permitted) should be used within the body of any redistributed or derivative code: "Copyright © 2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/"
.
3. Notice of any changes or modifications to the W3C files, including the date changes were made. (We recommend you provide URIs to the location from which the code is derived.)
.
Original W3C files; http://www.w3.org/2001/06/soap-encoding
Changes made:
- reverted namespace to http://schemas.xmlsoap.org/soap/encoding/
- reverted root to only allow 0 and 1 as lexical values
- removed default value from root attribute declaration
.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
.
COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
.
The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the software without specific, written prior permission. Title to copyright in this software and any associated documentation will at all times remain with copyright holders.

12
debian/gbp.conf vendored Normal file
View File

@ -0,0 +1,12 @@
# Settings for Debian Tryton Maintainer repositories
# for usage with git-buildpackage
[DEFAULT]
debian-branch = debian
pristine-tar = True
[buildpackage]
ignore-new = True
# Use export-dir at your discretion to avoid git-buildpackage messing
# your git repeository
#export-dir = ../build-area/

File diff suppressed because it is too large Load Diff

1
debian/patches/series vendored Normal file
View File

@ -0,0 +1 @@
01-add-missing-xsd-files.patch

1
debian/python-zeep.docs vendored Normal file
View File

@ -0,0 +1 @@
examples/

1
debian/python3-zeep.docs vendored Normal file
View File

@ -0,0 +1 @@
examples/

16
debian/rules vendored Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/make -f
export PYBUILD_NAME := zeep
%:
dh $@ --with python2,python3 --buildsystem=pybuild
override_dh_auto_clean:
rm -rf $(PACKAGE_NAME).egg-info
rm -rf PKG-INFO
dh_auto_clean
override_dh_install:
dh_install
# asyncio is Python3 only
rm -rf debian/python-zeep/usr/lib/python2.7/dist-packages/zeep/asyncio

4
debian/source.lintian-overrides vendored Normal file
View File

@ -0,0 +1,4 @@
# Missing test files in the tarball on pypi, but present in the
# github repository need to be mentioned in copyright.
python-zeep source: wildcard-matches-nothing-in-dep5-copyright
python-zeep source: unused-file-paragraph-in-dep5-copyright

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (quilt)

48
debian/upstream/signing-key.asc vendored Normal file
View File

@ -0,0 +1,48 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: SKS 1.1.5
Comment: Hostname: pgp.mit.edu
mQINBFcEG3kBEADGwYCsUz9ZtnelD/rVIN/cH+sLWydYCNRIFjO2Bw/7Bno2lzNyyzW3J+rG
dNuGnyLAts1S6Ofey13j4L7iWD6InKGOr6chOtKOBGHhDnbTaZNPR8AZOIr0wY/C1c+mIM/X
trPYuKYcaaTlFpVTGXCvXLnzJmrzsli1zLTEVgMTWhlbyIno3yZ+GOv7GLTAkEfUwl+9x2Gr
3sN87VgCcz0xrebNbY7OJSnR2BteljMQsot1oP/lKiRZgz51i0Bi37uHsybgHalbUljfZqYT
EEZ/C5t7NNh6H4qP/Zbt7vckXlHOLmYEW5nO5pL+T3dd3spZW04dwem/JMyOl+4aUNvAJQD0
l0i+Fk2CGGHyYpfNgq9zyEkEVASihBnXoUHwe9XbKtDhRQQDg56N7xJ2yvmKrK3B1aFhdpUb
xuSKWHeR3Fd6gE8UqEOpS/ZmhijYUuXG1NK1q4YbIv6I7VowxSx18KTetZgRc0dzUBU4Zh9R
+x6SmssPe7qAYUYWwv89hLTiTMScb8ov6yOHEangXneg81v+8IuKi258qALLr+nPVaFOpdIL
AekNvyWa1TMDhy5utpFcXDzTp8LFmX4ZWgJ7/ZLISPA7bBrXjf8cj76lx2YxpxJw4jBELfj6
lJVg1KVKJOAGRhEtX2Ref7vMtzexye38rABQH+YWM+U5GDKM+wARAQABtDBNaWNoYWVsIHZh
biBUZWxsaW5nZW4gPG1pY2hhZWxAbXZhbnRlbGxpbmdlbi5ubD6JAjgEEwECACIFAlcEG3kC
GwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJa2cEKrxE93wZ8QALPQjj3FjTQvw35B
0Sjuw08OQBPchh5q1SnlOA09Y0mxrRcC/UfXJsJ63OE0G8ITzipMj8GQskIARAAqIQSTiOLI
csCXDVpPesVEYgKqKAck8ic2vWqQdyE6jUaNGDAzvFzvtV5O/1tibm49s+G6r2rpSNvl8AYI
Oq6zR/sa8T4JlyetICF+5iC5eyza5DyGzBvIt0Ji0JasiWAV9GHNe3Z+P3lfglzQzQD4mz8Z
CeSEokoxKGZSp4b9cbs80Cj9Kc4ASaUNqC7yYo3vpGrlEF4MzeGGuauwhOzKo7A4SHtutAJb
98aGNSrb2wI7ReykaXbMIqKgB8i3UkwL8piMUhUHSKjdRJY7+suS01+cbntotGHhbp7vfp/a
FcKM9eo8kGdblvpdDd0MQDBPREuU3wV3Ldnuzl33/AYGHR811DkqGjsfSuiASy7IkzGirLxU
igflORRxdnp0K9h9zrStgF0LpnQFmfQRALfSHj0HGpxcHNKbg1aHkZGSr4+SSO00TJO5t7Gi
ioCYLooShB906PJd1W8ARbieH+tesY2ZZkL+WdsxWdBsvzMcHJcbq7zynn4P3KxNt4HouzEw
q0j/0UyoHNMeqyMvC6wTQ09Zih8s3bRfcBVVujv/RqSpHGN4F6dCtG/yibpaV72wXSgfCJag
+PfYWLqOJ9bsXCzkIAAiuQINBFcEG3kBEAC9BTDICB0cS9HOOzYsOAigV9i9Yw6Jc9KvcurI
U39cs/mbs5Fx+kC/q00+uitKMFgN9MFhdXxbHHZCDwEXv3N/+KBHiU1M15/ieuBdmm8KRvkj
6ztJQlZBVX3LXUxTAr5wJZQWXdnP6xNa74aVGCgPQemPfYpW6kdOv+sXxoKTEjJ5ME7AWQe2
K5sg6c4NmybECdnAlxdb0KGIpWQC9BeSaf5zNGOROpuQdh6TqHvG511KASHEWWzqXSxs6wXI
bbOCkqWvx7R5Pd3yoAhA8WZ2+YH4JTCv4KTiP8yWsuvaESxt3vPspG/q6EhyXU1t/hXeU0st
/lB0tcf/tYnltoeJvyF4dlbtqZm57F2c+FGmZnuWKpHQLRGfG4DedmhnUnLJRYLIGrCUuhG6
ExpVqPvZ1j2914TpkU+BELb4vRpIgFawjqcm+7dkEXynnYZrcXE1+T7+vnrV3cSFIEwGhv6N
hZ0X5j8b/iacGEhA5YqrpCiH+0Kj4h4MQmpKLPkFhB2fpMWij6bby0UaNOLY3q/li3zhonOh
m+WM8MkFPYMCg3p4fNlzsWCUjZNOqXVl5+8WxcBP3CT55cYN19BjmqbMW0z2QE3b9VNfzFMA
c548Lqmjqo3JgJkdZv81ZScn1XuKX56Lckz0iEcMfHSQWr0pDiN43SBn3iRgAvN0jWu2awAR
AQABiQIfBBgBAgAJBQJXBBt5AhsMAAoJEJa2cEKrxE93WVgQALtPxFRXhgdidE0apFT3AIQG
El45GBnItmDXVGcnx4X1mImWa01af0/Y3Znsg8NfrV+ud47adKysYDhuPeN5f7jaWa6RLdfL
KeKlD6j+8mnAvrXg+WV+/9nvuCrlfX7KIHlIGlWINDNjfvzBPZf15YVBLMO41s1L5bZMs/8A
Q5O8Ij3ct4SYSW6/2ZfP9oVhHbVeapkZtJAG2tnc9vwq2BHoOz7p8pj2T9qLxbYWiFVKI16g
GhknUydyqNk3FJz4T1GlAMKujGypfTCh/+3Kw6jDST9vGs6hybb4UA1u9Lvh4LgsNSjiBzI2
+wR3dsqkgUCh3ONHGdwNFdCz9SFP6cFpm39x6gucDkJQfTm/mDkDTLr/c8w3QMg3QGokIgYR
2Csli7DASaOSuOtW+fHudVUnN4wsSjzT0P4xGxj3IM0fSbyrSq5VlQxLHb97Qhh8WOvgAzFQ
NRZzkNVWG8tUhj66Dpoh+c0m04JhN/CMbLRZ7tZFYqH+GzFcNS75TKmQpfVSyNMAmV+4rP1a
odNHJpe67VglkA6ix4Bpv+TN+E9hHpvcKYEFxV2uOVfFpEltb4dmerDo/iCA6Wy2qyrLrqgu
0OwiZtYDJ92VjHFX8h0w4q8cV2QSnI0miGrTxovzubdVTn7Um/UZ4PoF1jIahXiwlRpOdn7n
mbtC2StQre7+
=gI+u
-----END PGP PUBLIC KEY BLOCK-----

3
debian/watch vendored Normal file
View File

@ -0,0 +1,3 @@
version=3
opts=uversionmangle=s/(rc|a|b|c)/~$1/,pgpsigurlmangle=s/$/.asc/ \
https://pypi.debian.net/zeep/zeep-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))

216
docs/Makefile Normal file
View File

@ -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."

18
docs/_templates/sidebar-intro.html vendored Normal file
View File

@ -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>

299
docs/conf.py Normal file
View File

@ -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

131
docs/index.rst Normal file
View File

@ -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@mvantellingen.nl for more information.
.. _let me know: https://github.com/mvantellingen/python-zeep/issues

263
docs/make.bat Normal file
View File

@ -0,0 +1,263 @@
@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

8
examples/code39.py Normal file
View File

@ -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))

View File

@ -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'))

View File

@ -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'))

16
examples/km_to_miles.py Normal file
View File

@ -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'))

11
setup.cfg Normal file
View File

@ -0,0 +1,11 @@
[wheel]
universal = 1
[flake8]
max-line-length = 99
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

72
setup.py Executable file
View File

@ -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@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,
)

5
src/zeep/__init__.py Normal file
View File

@ -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'

85
src/zeep/__main__.py Normal file
View File

@ -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)

View File

@ -0,0 +1,2 @@
from .transport import * # noqa
from .bindings import * # noqa

View File

@ -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

View File

@ -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)

158
src/zeep/cache.py Normal file
View File

@ -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')

188
src/zeep/client.py Normal file
View File

@ -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

49
src/zeep/exceptions.py Normal file
View File

@ -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

20
src/zeep/helpers.py Normal file
View File

@ -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

40
src/zeep/parser.py Normal file
View File

@ -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

63
src/zeep/plugins.py Normal file
View File

@ -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,
},
})

154
src/zeep/transports.py Normal file
View File

@ -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

67
src/zeep/utils.py Normal file
View File

@ -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

42
src/zeep/wsa.py Normal file
View File

@ -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

View File

@ -0,0 +1 @@
from zeep.wsdl.wsdl import Document # noqa

View File

@ -0,0 +1,2 @@
from .soap import Soap11Binding, Soap12Binding # noqa
from .http import HttpGetBinding, HttpPostBinding # noqa

View File

@ -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)

View File

@ -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)

View File

@ -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
@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()
@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()
@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
@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

View File

@ -0,0 +1,3 @@
from .http import * # noqa
from .mime import * # noqa
from .soap import * # noqa

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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}

164
src/zeep/wsdl/parse.py Normal file
View File

@ -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

19
src/zeep/wsdl/utils.py Normal file
View File

@ -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')

387
src/zeep/wsdl/wsdl.py Normal file
View File

@ -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

View File

118
src/zeep/wsse/username.py Normal file
View File

@ -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)
]

30
src/zeep/wsse/utils.py Normal file
View File

@ -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()

6
src/zeep/xsd/__init__.py Normal file
View File

@ -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

676
src/zeep/xsd/builtins.py Normal file
View File

@ -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(),
}

12
src/zeep/xsd/const.py Normal file
View File

@ -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)

49
src/zeep/xsd/context.py Normal file
View File

@ -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 = []

500
src/zeep/xsd/elements.py Normal file
View File

@ -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)

565
src/zeep/xsd/indicators.py Normal file
View File

@ -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)

42
src/zeep/xsd/parser.py Normal file
View File

@ -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)

67
src/zeep/xsd/printer.py Normal file
View File

@ -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)

383
src/zeep/xsd/schema.py Normal file
View File

@ -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)

597
src/zeep/xsd/types.py Normal file
View File

@ -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)
@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

33
src/zeep/xsd/utils.py Normal file
View File

@ -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)

View File

@ -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

983
src/zeep/xsd/visitor.py Normal file
View File

@ -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

2
tests/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from zeep.client import Client # noqa
from zeep.exceptions import Fault # noqa

14
tests/conftest.py Normal file
View File

@ -0,0 +1,14 @@
import pytest
pytest.register_assert_rewrite('tests.utils')
@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)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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()

View File

@ -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()

46
tests/test_cache.py Normal file
View File

@ -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'

254
tests/test_client.py Normal file
View File

@ -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
@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'))
@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
@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
@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
@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

113
tests/test_helpers.py Normal file
View File

@ -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'

89
tests/test_response.py Normal file
View File

@ -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

828
tests/test_wsdl.py Normal file
View File

@ -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
@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)
@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)
@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)

587
tests/test_xsd.py Normal file
View File

@ -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')

340
tests/test_xsd_builtins.py Normal file
View File

@ -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'

View File

@ -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@example.com')
assert obj.NameFirst == 'John'
assert obj.NameLast == 'Doe'
assert obj.Email == 'j.doe@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@example.com')
assert obj.NameFirst == 'John'
assert obj.NameLast == 'Doe'
assert obj.Email == 'j.doe@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'

859
tests/test_xsd_parse.py Normal file
View File

@ -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)

579
tests/test_xsd_visitor.py Normal file
View File

@ -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'

44
tests/utils.py Normal file
View File

@ -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)

View File

@ -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>

115
tests/wsdl_files/soap.wsdl Normal file
View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More