Merging upstream version 2.1.1.

This commit is contained in:
Mathias Behrle 2017-06-12 18:33:14 +02:00
parent cad63e607f
commit 1b75311cb8
86 changed files with 4973 additions and 1552 deletions

200
CHANGES
View File

@ -1,3 +1,119 @@
2.1.1 (2017-06-11)
------------------
- Fix previous release, it contained an incorrect dependency (Mock 2.1.) due
to bumpversion :-(
2.1.0 (2017-06-11)
------------------
- Fix recursion error while creating the signature for a global element when
it references itself (via ref attribute).
- Update Client.create_message() to apply plugins and wsse (#465)
- Fix handling unknown xsi types when parsing elements using xsd:anyType (#455)
2.0.0 (2017-05-22)
------------------
This is a major release, and contains a number of backwards incompatible
changes to the API.
- Default values of optional elements are not set by default anymore (#423)
- Refactor the implementation of wsdl:arrayType too make the API more
pythonic (backwards incompatible).
- The call signature for Client.create_message() was changed. It now requires
the service argument:
``Client.create_message(service, operation_name, *args, **kwargs)``
- Choice elements now only work with keyword arguments and raise an exception
if positional arguments are passed (#439)
- Implement initial multiref support for SOAP RPC (#326). This was done using
really good real-world tests from vstoykov (thanks!)
- Fix exception on empty SOAP response (#442)
- Fix XSD default values for boolean types (Bartek Wójcicki, #386)
1.6.0 (2017-04-27)
------------------
- Implement ValueObject.__json__ for json serialization (#258)
- Improve handling of unexpected elements for soap:header (#378)
- Accept unexpected elements in complexTypes when strict is False
- Fix elementFormDefault/attributeFormDefault for xsd:includes (#426)
1.5.0 (2017-04-22)
------------------
- Fix issue where values of indicators (sequence/choice/all) would
write to the same internal dict. (#425)
- Set default XML parse mode to strict as was intended (#332)
- Add support for pickling value objects (#417)
- Add explicit Nil value via ``zeep.xsd.Nil`` (#424)
- Add xml_huge_tree kwarg to the Client() to enable lxml's huge_tree mode,
this is disabled by default (#332)
- Add support to pass base-types to type extensions (#416)
- Handle wsdl errors more gracefully by disabling the invalid operations
instead of raising an exception (#407, #387)
1.4.1 (2017-04-01)
------------------
- The previous release (1.4.0) contained an incorrect dependency due to
bumpversion moving all 1.3.0 versions to 1.4.0. This fixes it.
1.4.0 (2017-04-01)
------------------
- Hardcode the xml prefix to the xml namespace as defined in the specs (#367)
- Fix parsing of unbound sequences within xsd choices (#380)
- Use logger.debug() for debug related logging (#369)
- Add the ``Client.raw_response`` option to let zeep return the raw
transport response (requests.Response) instead of trying to parse it.
- Handle minOccurs/maxOccurs properlhy for xsd:Group elements. This also
fixes a bug in the xsd:Choice handling for multiple elements (#374, #410)
- Fix raising XMLSyntaxError when loading invalid XML (Antanas Sinica, #396)
1.3.0 (2017-03-14)
------------------
- Add support for nested xsd:choice elements (#370)
- Fix unresolved elements for xsd:extension, this was a regression introduced
in 1.2.0 (#377)
1.2.0 (2017-03-12)
------------------
- Add flag to disable strict mode in the Client. This allows zeep to better
work with non standard compliant SOAP Servers. See the documentation for
usage and potential downsides.
- Minor refactor of resolving of elements for improved performance
- Support the SOAP 1.2 'http://www.w3.org/2003/05/soap/bindings/HTTP/'
transport uri (#355)
- Fallback to matching wsdl lookups to matching when the target namespace is
empty (#356)
- Improve the handling of xsd:includes, the default namespace of the parent
schema is now also used during resolving of the included schema. (#360)
- Properly propagate the global flag for complex types when an xsd:restriction
is used (#360)
- Filter out duplicate types and elements when dump the wsdl schema (#360)
- Add ``zeep.CachingClient()`` which enables the SqliteCache by default
1.1.0 (2017-02-18)
------------------
- Fix an attribute error when an complexType used xsd:anyType as base
restriction (#352)
- Update asyncio transport to return requests.Response objects (#335)
1.0.0 (2017-01-31)
------------------
- Use cgi.parse_header() to extract media_type for multipart/related checks
(#327)
- Don't ignore nil elements, instead return None when parsing xml (#328)
- Fix regression when using WSA with an older lxml version (#197)
0.27.0 (2017-01-28)
-------------------
- Add support for SOAP attachments (multipart responses). (Dave Wapstra, #302)
@ -11,13 +127,13 @@
This release again introduces some backwords incompatibilties. The next release
will hopefully be 1.0 which will introduce semver.
- **backwards-incompatible**: The Transport class now accepts a
- **backwards-incompatible**: The Transport class now accepts a
``requests.Session()`` object instead of ``http_auth`` and ``verify``. This
allows for more flexibility.
- **backwards-incompatible**: Zeep no longer sets a default cache backend.
Please see http://docs.python-zeep.org/en/master/transport.html#caching for
information about how to configure a cache.
- Add ``zeep.xsd.SkipValue`` which instructs the serialize to ignore the
- Add ``zeep.xsd.SkipValue`` which instructs the serialize to ignore the
element.
- Support duplicate target namespaces in the wsdl definition (#320)
- Fix resolving element/types for xsd schema's with duplicate tns (#319)
@ -40,12 +156,12 @@ will hopefully be 1.0 which will introduce semver.
type. Instead log a message (#273)
- Fix serializing etree.Element instances in the helpers.serialize function
(#255)
- Fix infinite loop during parsing of xsd.Sequence where max_occurs is
- Fix infinite loop during parsing of xsd.Sequence where max_occurs is
unbounded (#256)
- Make the xsd.Element name kwarg required
- Improve handling of the xsd:anyType element when passed instances of
- Improve handling of the xsd:anyType element when passed instances of
complexType's (#252)
- Silently ignore unsupported binding transports instead of an hard error
- Silently ignore unsupported binding transports instead of an hard error
(#277)
- Support microseconds for xsd.dateTime and xsd.Time (#280)
- Don't mutate passed values to the zeep operations (#280)
@ -56,7 +172,7 @@ will hopefully be 1.0 which will introduce semver.
- 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
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)
@ -65,7 +181,7 @@ will hopefully be 1.0 which will introduce semver.
0.22.1 (2016-11-22)
-------------------
- Fix reversed() error (jaceksnet) (#260)
- Better error message when unexpected xml elements are encountered in
- Better error message when unexpected xml elements are encountered in
sequences.
@ -74,13 +190,13 @@ will hopefully be 1.0 which will introduce semver.
- 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
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
- 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.
simplify creation of types.
@ -88,15 +204,15 @@ will hopefully be 1.0 which will introduce semver.
-------------------
- 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__
- Don't use pkg_resources to determine the zeep version, use __version__
instead (#243).
- Fix SOAP arrays by wrapping children in the appropriate element
- 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
- 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
@ -106,7 +222,7 @@ will hopefully be 1.0 which will introduce semver.
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
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.
@ -118,12 +234,12 @@ will hopefully be 1.0 which will introduce semver.
-------------------
- **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
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
- 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)
@ -139,7 +255,7 @@ will hopefully be 1.0 which will introduce semver.
0.18.0 (2016-09-23)
-------------------
- Fix parsing Any elements by using the namespace map of the response node
- 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)
@ -149,14 +265,14 @@ will hopefully be 1.0 which will introduce semver.
- 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
- 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,
- 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
@ -183,7 +299,7 @@ will hopefully be 1.0 which will introduce semver.
0.14.0 (2016-08-03)
-------------------
- Global attributes are now always correctly handled as qualified. (#129)
- Fix parsing xml data containing simpleContent types (#136).
- 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)
@ -199,8 +315,8 @@ will hopefully be 1.0 which will introduce semver.
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
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
@ -209,30 +325,30 @@ will hopefully be 1.0 which will introduce semver.
0.12.0 (2016-07-09)
-------------------
- **backwards-incompatible**: Choice elements are now unwrapped if
- **backwards-incompatible**: Choice elements are now unwrapped if
maxOccurs=1. This results in easier operation definitions when choices are
used.
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
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
- **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.
- 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.
@ -240,12 +356,12 @@ will hopefully be 1.0 which will introduce semver.
0.10.0 (2016-06-22)
-------------------
- Make global elements / types truly global by refactoring the Schema
- 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
- Update exceptions structure, all zeep exceptions are now using
zeep.exceptions.Error() as base class.
@ -253,12 +369,12 @@ will hopefully be 1.0 which will introduce semver.
------------------
- 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
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)
@ -273,7 +389,7 @@ will hopefully be 1.0 which will introduce semver.
0.8.1 (2016-06-08)
------------------
- Use the operation name for the xml element which wraps the parameters in
- Use the operation name for the xml element which wraps the parameters in
for soap RPC messages (#60)
@ -281,7 +397,7 @@ will hopefully be 1.0 which will introduce semver.
------------------
- 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
- 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)
@ -298,9 +414,9 @@ will hopefully be 1.0 which will introduce semver.
------------------
- 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
- Fix issue where setting cache=None to Transport class didn't disable
caching.
- Refactor handling of wsdl:imports, don't merge definitions but instead
- 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)
@ -323,14 +439,14 @@ will hopefully be 1.0 which will introduce semver.
------------------
- 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
- Implement support for WSSE usernameToken profile including
passwordText/passwordDigest.
- Improve XSD date/time related builtins.
- Various minor XSD handling fixes
- 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()
- **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ł)
@ -361,7 +477,7 @@ will hopefully be 1.0 which will introduce semver.
------------------
- Improve xsd.DateTime, xsd.Date and xsd.Time implementations by using the
isodate module.
- Implement xsd.Duration
- Implement xsd.Duration
0.2.3 (2016-04-03)

45
CONTRIBUTORS.rst Normal file
View File

@ -0,0 +1,45 @@
Authors
=======
* Michael van Tellingen
Contributors
============
* vashek
* Marco Vellinga
* jaceksnet
* Andrew Serong
* Joeri Bekker
* Eric Wong
* Jacek Stępniewski
* Alexey Stepanov
* Julien Delasoie
* bjarnagin
* mcordes
* Sam Denton
* David Baumgold
* fiebiga
* Antonio Cuni
* Alexandre de Mari
* Jason Vertrees
* Nicolas Evrard
* Matt Grimm (mgrimm)
* Marek Wywiał
* Falldog
* btmanm
* Caleb Salt
* Julien Marechal
* Mike Fiedler
* Dave Wapstra
* OrangGeeGee
* Stefano Parmesan
* Jan Murre
* Ben Tucker
* Bruno Duyé
* Christoph Heuel
* Derek Harland
* Eric Waller
* Falk Schuetzenmeister
* Jon Jenkins
* Raymond Piller
* Zoltan Benedek
* Øyvind Heddeland Instefjord

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Michael van Tellingen
Copyright (c) 2016-2017 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

19
MANIFEST.in Normal file
View File

@ -0,0 +1,19 @@
# Exclude everything by default
exclude *
recursive-exclude * *
include MANIFEST.in
include CHANGES
include CONTRIBUTORS.rst
include LICENSE
include README.rst
include setup.cfg
include setup.py
graft examples
graft src
graft tests
global-exclude __pycache__
global-exclude *.py[co]
global-exclude .DS_Store

View File

@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: zeep
Version: 0.27.0
Version: 2.1.1
Summary: A modern/fast Python SOAP client based on lxml / requests
Home-page: http://docs.python-zeep.org
Author: Michael van Tellingen
@ -57,18 +57,16 @@ Description: ========================
Support
=======
If you encounter bugs then please `let me know`_ . A copy of the WSDL file if
possible would be most helpful.
If you want to report a bug then please first read
http://docs.python-zeep.org/en/master/reporting_bugs.html
I'm also able to offer commercial support. As in contracting work. Please
contact me at info@mvantellingen.nl for more information. If you just have a
random question and don't intent to actually pay me for my support then please
DO NOT email me at that e-mail address but just use stackoverflow or something..
.. _let me know: https://github.com/mvantellingen/python-zeep/issues
contact me at info@mvantellingen.nl for more information. Note that asking
questions or reporting bugs via this e-mail address will be ignored. Pleae use
the appropriate channels for that (e.g. stackoverflow)
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7

View File

@ -72,12 +72,10 @@ information.
Support
=======
If you encounter bugs then please `let me know`_ . A copy of the WSDL file if
possible would be most helpful.
If you want to report a bug then please first read
http://docs.python-zeep.org/en/master/reporting_bugs.html
I'm also able to offer commercial support. As in contracting work. Please
contact me at info@mvantellingen.nl for more information. If you just have a
random question and don't intent to actually pay me for my support then please
DO NOT email me at that e-mail address but just use stackoverflow or something..
.. _let me know: https://github.com/mvantellingen/python-zeep/issues
contact me at info@mvantellingen.nl for more information. Note that asking
questions or reporting bugs via this e-mail address will be ignored. Pleae use
the appropriate channels for that (e.g. stackoverflow)

View File

@ -1,10 +1,16 @@
from __future__ import print_function
from requests import Session
from requests.auth import HTTPBasicAuth
import zeep
from zeep.transports import Transport
# Example using basic authentication with a webservice
transport_with_basic_auth = Transport(http_auth=('username', 'password'))
session = Session()
session.auth = HTTPBasicAuth('username', 'password')
transport_with_basic_auth = Transport(session=session)
client = zeep.Client(
wsdl='http://nonexistent?WSDL',

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.27.0
current_version = 2.1.1
commit = true
tag = true
tag_name = {new_version}
@ -19,6 +19,8 @@ max-line-length = 99
[bumpversion:file:docs/conf.py]
[bumpversion:file:docs/index.rst]
[bumpversion:file:src/zeep/__init__.py]
[coverage:run]
@ -38,5 +40,4 @@ show_missing = True
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

View File

@ -1,15 +1,16 @@
import re
import sys
from setuptools import find_packages, setup
install_requires = [
'appdirs>=1.4.0',
'cached-property>=1.0.0',
'cached-property>=1.3.0',
'defusedxml>=0.4.1',
'isodate>=0.5.4',
'lxml>=3.0.0',
'requests>=2.7.0',
'requests-toolbelt>=0.7.0',
'requests-toolbelt>=0.7.1',
'six>=1.9.0',
'pytz',
]
@ -18,9 +19,7 @@ docs_require = [
'sphinx>=1.4.0',
]
async_require = [
'aiohttp>=1.0',
]
async_require = [] # see below
xmlsec_require = [
'xmlsec>=0.6.1',
@ -41,13 +40,19 @@ tests_require = [
'flake8-debugger==1.4.0',
]
if sys.version_info > (3, 4, 2):
async_require.append('aiohttp>=1.0')
tests_require.append('aioresponses>=0.1.3')
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.27.0',
version='2.1.1',
description='A modern/fast Python SOAP client based on lxml / requests',
long_description=long_description,
author="Michael van Tellingen",
@ -69,7 +74,7 @@ setup(
license='MIT',
classifiers=[
'Development Status :: 4 - Beta',
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',

View File

@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: zeep
Version: 0.27.0
Version: 2.1.1
Summary: A modern/fast Python SOAP client based on lxml / requests
Home-page: http://docs.python-zeep.org
Author: Michael van Tellingen
@ -57,18 +57,16 @@ Description: ========================
Support
=======
If you encounter bugs then please `let me know`_ . A copy of the WSDL file if
possible would be most helpful.
If you want to report a bug then please first read
http://docs.python-zeep.org/en/master/reporting_bugs.html
I'm also able to offer commercial support. As in contracting work. Please
contact me at info@mvantellingen.nl for more information. If you just have a
random question and don't intent to actually pay me for my support then please
DO NOT email me at that e-mail address but just use stackoverflow or something..
.. _let me know: https://github.com/mvantellingen/python-zeep/issues
contact me at info@mvantellingen.nl for more information. Note that asking
questions or reporting bugs via this e-mail address will be ignored. Pleae use
the appropriate channels for that (e.g. stackoverflow)
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7

View File

@ -1,5 +1,7 @@
CHANGES
CONTRIBUTORS.rst
LICENSE
MANIFEST.in
README.rst
setup.cfg
setup.py
@ -16,8 +18,8 @@ src/zeep/cache.py
src/zeep/client.py
src/zeep/exceptions.py
src/zeep/helpers.py
src/zeep/loader.py
src/zeep/ns.py
src/zeep/parser.py
src/zeep/plugins.py
src/zeep/transports.py
src/zeep/utils.py
@ -45,6 +47,7 @@ src/zeep/wsdl/messages/__init__.py
src/zeep/wsdl/messages/base.py
src/zeep/wsdl/messages/http.py
src/zeep/wsdl/messages/mime.py
src/zeep/wsdl/messages/multiref.py
src/zeep/wsdl/messages/soap.py
src/zeep/wsse/__init__.py
src/zeep/wsse/compose.py
@ -74,6 +77,7 @@ src/zeep/xsd/types/builtins.py
src/zeep/xsd/types/collection.py
src/zeep/xsd/types/complex.py
src/zeep/xsd/types/simple.py
src/zeep/xsd/types/unresolved.py
tests/__init__.py
tests/cert_valid.pem
tests/cert_valid_pw.pem
@ -83,9 +87,11 @@ tests/test_cache.py
tests/test_client.py
tests/test_client_factory.py
tests/test_helpers.py
tests/test_loader.py
tests/test_main.py
tests/test_pprint.py
tests/test_response.py
tests/test_soap_multiref.py
tests/test_transports.py
tests/test_wsa.py
tests/test_wsdl.py
@ -101,9 +107,12 @@ tests/test_xsd.py
tests/test_xsd_any.py
tests/test_xsd_attributes.py
tests/test_xsd_builtins.py
tests/test_xsd_choice.py
tests/test_xsd_complex_types.py
tests/test_xsd_extension.py
tests/test_xsd_indicators_all.py
tests/test_xsd_indicators_choice.py
tests/test_xsd_indicators_group.py
tests/test_xsd_indicators_sequence.py
tests/test_xsd_integration.py
tests/test_xsd_parse.py
tests/test_xsd_schemas.py

View File

@ -1,10 +1,10 @@
appdirs>=1.4.0
cached-property>=1.0.0
cached-property>=1.3.0
defusedxml>=0.4.1
isodate>=0.5.4
lxml>=3.0.0
requests>=2.7.0
requests-toolbelt>=0.7.0
requests-toolbelt>=0.7.1
six>=1.9.0
pytz
@ -25,6 +25,7 @@ isort==4.2.5
flake8==3.2.1
flake8-blind-except==0.1.1
flake8-debugger==1.4.0
aioresponses>=0.1.3
[xmlsec]
xmlsec>=0.6.1

View File

@ -1,6 +1,6 @@
from zeep.client import Client # noqa
from zeep.client import CachingClient, Client # noqa
from zeep.transports import Transport # noqa
from zeep.plugins import Plugin # noqa
from zeep.xsd.valueobjects import AnyObject # noqa
__version__ = '0.27.0'
__version__ = '2.1.1'

View File

@ -7,6 +7,7 @@ import time
import requests
from six.moves.urllib.parse import urlparse
from zeep.cache import SqliteCache
from zeep.client import Client
from zeep.transports import Transport
@ -27,6 +28,9 @@ def parse_arguments(args=None):
'--verbose', action='store_true', help='Enable verbose output')
parser.add_argument(
'--profile', help="Enable profiling and save output to given file")
parser.add_argument(
'--no-strict', action='store_true', default=False,
help="Disable strict mode")
return parser.parse_args(args)
@ -72,7 +76,9 @@ def main(args):
transport = Transport(cache=cache, session=session)
st = time.time()
client = Client(args.wsdl_file, transport=transport)
strict = not args.no_strict
client = Client(args.wsdl_file, transport=transport, strict=strict)
logger.debug("Loading WSDL took %sms", (time.time() - st) * 1000)
if args.profile:

View File

@ -6,6 +6,8 @@ import asyncio
import logging
import aiohttp
from requests import Response
from zeep.transports import Transport
from zeep.utils import get_version
from zeep.wsdl.utils import etree_to_string
@ -27,9 +29,14 @@ class AsyncTransport(Transport):
self.logger = logging.getLogger(__name__)
self.session = session or aiohttp.ClientSession(loop=self.loop)
self._close_session = session is None
self.session._default_headers['User-Agent'] = (
'Zeep/%s (www.python-zeep.org)' % (get_version()))
def __del__(self):
if self._close_session:
self.session.close()
def _load_remote_data(self, url):
result = None
@ -56,20 +63,21 @@ class AsyncTransport(Transport):
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)
return await self.new_response(response)
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)
return await self.new_response(response)
async def new_response(self, response):
"""Convert an aiohttp.Response object to a requests.Response object"""
new = Response()
new._content = await response.read()
new.status_code = response.status
new.headers = response.headers
new.cookies = response.cookies
new.encoding = response.charset
return new

View File

@ -4,7 +4,7 @@ from contextlib import contextmanager
from zeep.transports import Transport
from zeep.wsdl import Document
from zeep.xsd.const import NotSet
logger = logging.getLogger(__name__)
@ -15,6 +15,12 @@ class OperationProxy(object):
self._op_name = operation_name
def __call__(self, *args, **kwargs):
"""Call the operation with the given args and kwargs.
:rtype: zeep.xsd.CompoundValue
"""
if self._proxy._client._default_soapheaders:
op_soapheaders = kwargs.get('_soapheaders')
if op_soapheaders:
@ -42,9 +48,19 @@ class ServiceProxy(object):
self._binding = binding
def __getattr__(self, key):
"""Return the OperationProxy for the given key.
:rtype: OperationProxy()
"""
return self[key]
def __getitem__(self, key):
"""Return the OperationProxy for the given key.
:rtype: OperationProxy()
"""
try:
self._binding.get(key)
except ValueError:
@ -62,9 +78,19 @@ class Factory(object):
self._ns = types.get_ns_prefix(namespace)
def __getattr__(self, key):
"""Return the complexType or simpleType for the given localname.
:rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType
"""
return self[key]
def __getitem__(self, key):
"""Return the complexType or simpleType for the given localname.
:rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType
"""
return self._method('{%s}%s' % (self._ns, key))
@ -81,19 +107,26 @@ class Client(object):
first port defined in the service element in the WSDL
document.
:param plugins: a list of Plugin instances
:param xml_huge_tree: disable lxml/libxml2 security restrictions and
support very deep trees and very long text content
"""
def __init__(self, wsdl, wsse=None, transport=None,
service_name=None, port_name=None, plugins=None):
service_name=None, port_name=None, plugins=None,
strict=True, xml_huge_tree=False):
if not wsdl:
raise ValueError("No URL given for the wsdl")
self.transport = transport or Transport()
self.wsdl = Document(wsdl, self.transport)
self.transport = transport if transport is not None else Transport()
self.wsdl = Document(wsdl, self.transport, strict=strict)
self.wsse = wsse
self.plugins = plugins if plugins is not None else []
self.xml_huge_tree = xml_huge_tree
# options
self.raw_response = False
self._default_service = None
self._default_service_name = service_name
@ -102,7 +135,11 @@ class Client(object):
@property
def service(self):
"""The default ServiceProxy instance"""
"""The default ServiceProxy instance
:rtype: ServiceProxy
"""
if self._default_service:
return self._default_service
@ -116,7 +153,7 @@ class Client(object):
return self._default_service
@contextmanager
def options(self, timeout):
def options(self, timeout=NotSet, raw_response=NotSet):
"""Context manager to temporarily overrule various options.
:param timeout: Set the timeout for POST/GET operations (not used for
@ -130,8 +167,22 @@ class Client(object):
"""
with self.transport._options(timeout=timeout):
yield
# Store current options
old_raw_raw_response = self.raw_response
# Set new options
self.raw_response = raw_response
if timeout is not NotSet:
timeout_ctx = self.transport._options(timeout=timeout)
timeout_ctx.__enter__()
yield
self.raw_response = old_raw_raw_response
if timeout is not NotSet:
timeout_ctx.__exit__(None, None, None)
def bind(self, service_name=None, port_name=None):
"""Create a new ServiceProxy for the given service_name and port_name.
@ -163,15 +214,14 @@ class Client(object):
"are: %s" % (', '.join(self.wsdl.bindings.keys())))
return ServiceProxy(self, binding, address=address)
def create_message(self, operation, service_name=None, port_name=None,
args=None, kwargs=None):
"""Create the payload for the given operation."""
service = self._get_service(service_name)
port = self._get_port(service, port_name)
def create_message(self, service, operation_name, *args, **kwargs):
"""Create the payload for the given operation.
args = args or tuple()
kwargs = kwargs or {}
envelope, http_headers = port.binding._create(operation, args, kwargs)
:rtype: lxml.etree._Element
"""
envelope, http_headers = service._binding._create(
operation_name, args, kwargs, client=self)
return envelope
def type_factory(self, namespace):
@ -182,15 +232,25 @@ class Client(object):
factory = client.type_factory('ns0')
user = factory.User(name='John')
:rtype: Factory
"""
return Factory(self.wsdl.types, 'type', namespace)
def get_type(self, name):
"""Return the type for the given qualified name."""
"""Return the type for the given qualified name.
:rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType
"""
return self.wsdl.types.get_type(name)
def get_element(self, name):
"""Return the element for the given qualified name."""
"""Return the element for the given qualified name.
:rtype: zeep.xsd.Element
"""
return self.wsdl.types.get_element(name)
def set_ns_prefix(self, prefix, namespace):
@ -227,3 +287,20 @@ class Client(object):
else:
service = next(iter(self.wsdl.services.values()), None)
return service
class CachingClient(Client):
"""Shortcut to create a caching client, for the lazy people.
This enables the SqliteCache by default in the transport as was the default
in earlier versions of zeep.
"""
def __init__(self, *args, **kwargs):
# Don't use setdefault since we want to lazily init the Transport cls
from zeep.cache import SqliteCache
kwargs['transport'] = (
kwargs.get('transport') or Transport(cache=SqliteCache()))
super(CachingClient, self).__init__(*args, **kwargs)

View File

@ -39,7 +39,11 @@ class TransportError(Error):
class LookupError(Error):
pass
def __init__(self, *args, **kwargs):
self.qname = kwargs.pop('qname', None)
self.item_name = kwargs.pop('item_name', None)
self.location = kwargs.pop('location', None)
super(LookupError, self).__init__(*args, **kwargs)
class NamespaceError(Error):
@ -74,3 +78,11 @@ class ValidationError(Error):
class SignatureVerificationFailed(Error):
pass
class IncompleteMessage(Error):
pass
class IncompleteOperation(Error):
pass

113
src/zeep/loader.py Normal file
View File

@ -0,0 +1,113 @@
import os.path
from defusedxml.lxml import fromstring
from lxml import etree
from six.moves.urllib.parse import urljoin, urlparse
from zeep.exceptions import XMLSyntaxError
class ImportResolver(etree.Resolver):
"""Custom lxml resolve to use the transport object"""
def __init__(self, transport):
self.transport = transport
def resolve(self, url, pubid, context):
if urlparse(url).scheme in ('http', 'https'):
content = self.transport.load(url)
return self.resolve_string(content, context)
def parse_xml(content, transport, base_url=None, strict=True,
xml_huge_tree=False):
"""Parse an XML string and return the root Element.
:param content: The XML string
:type content: str
:param transport: The transport instance to load imported documents
:type transport: zeep.transports.Transport
:param base_url: The base url of the document, used to make relative
lookups absolute.
:type base_url: str
:param strict: boolean to indicate if the lxml should be parsed a 'strict'.
If false then the recover mode is enabled which tries to parse invalid
XML as best as it can.
:param xml_huge_tree: boolean to indicate if lxml should process very
large XML content.
:type strict: boolean
:returns: The document root
:rtype: lxml.etree._Element
"""
recover = not strict
parser = etree.XMLParser(remove_comments=True, resolve_entities=False,
recover=recover, huge_tree=xml_huge_tree)
parser.resolvers.add(ImportResolver(transport))
try:
return fromstring(content, parser=parser, base_url=base_url)
except etree.XMLSyntaxError as exc:
raise XMLSyntaxError("Invalid XML content received (%s)" % exc.msg)
def load_external(url, transport, base_url=None, strict=True):
"""Load an external XML document.
:param url:
:param transport:
:param base_url:
:param strict: boolean to indicate if the lxml should be parsed a 'strict'.
If false then the recover mode is enabled which tries to parse invalid
XML as best as it can.
:type strict: boolean
"""
if hasattr(url, 'read'):
content = url.read()
else:
if base_url:
url = absolute_location(url, base_url)
content = transport.load(url)
return parse_xml(content, transport, base_url, strict=strict)
def absolute_location(location, base):
"""Make an url absolute (if it is optional) via the passed base url.
:param location: The (relative) url
:type location: str
:param base: The base location
:type base: str
:returns: An absolute URL
:rtype: str
"""
if location == base:
return location
if urlparse(location).scheme in ('http', 'https', 'file'):
return location
if base and urlparse(base).scheme in ('http', 'https', 'file'):
return urljoin(base, location)
else:
if os.path.isabs(location):
return location
if base:
return os.path.realpath(
os.path.join(os.path.dirname(base), location))
return location
def is_relative_path(value):
"""Check if the given value is a relative path
:param value: The value
:type value: str
:returns: Boolean indicating if the url is relative. If it is absolute then
False is returned.
:rtype: boolean
"""
if urlparse(value).scheme in ('http', 'https', 'file'):
return False
return not os.path.isabs(value)

View File

@ -4,6 +4,7 @@ SOAP_12 = 'http://schemas.xmlsoap.org/wsdl/soap12/'
SOAP_ENV_11 = 'http://schemas.xmlsoap.org/soap/envelope/'
SOAP_ENV_12 = 'http://www.w3.org/2003/05/soap-envelope'
XSI = 'http://www.w3.org/2001/XMLSchema-instance'
XSD = 'http://www.w3.org/2001/XMLSchema'
WSDL = 'http://schemas.xmlsoap.org/wsdl/'
@ -16,3 +17,7 @@ WSA = 'http://www.w3.org/2005/08/addressing'
DS = 'http://www.w3.org/2000/09/xmldsig#'
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'
NAMESPACE_TO_PREFIX = {
XSD: 'xsd',
}

View File

@ -1,49 +0,0 @@
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, resolve_entities=False)
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:
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.realpath(
os.path.join(os.path.dirname(base), location))
return location
def is_relative_path(value):
"""Check if the given value is a relative path"""
if urlparse(value).scheme in ('http', 'https', 'file'):
return False
return not os.path.isabs(value)

View File

@ -3,9 +3,9 @@ import os
from contextlib import contextmanager
import requests
from six.moves.urllib.parse import urlparse
from zeep.utils import get_version
from zeep.utils import get_media_type, get_version
from zeep.wsdl.utils import etree_to_string
@ -16,7 +16,7 @@ class Transport(object):
: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 session: A request.Session() object (optional)
:param session: A :py:class:`request.Session()` object (optional)
"""
supports_async = False
@ -68,7 +68,10 @@ class Transport(object):
timeout=self.operation_timeout)
if self.logger.isEnabledFor(logging.DEBUG):
if 'multipart/related' in response.headers.get('Content-Type'):
media_type = get_media_type(
response.headers.get('Content-Type', 'text/xml'))
if media_type == 'multipart/related':
log_message = response.content
else:
log_message = response.content

View File

@ -1,7 +1,11 @@
import cgi
import inspect
from lxml import etree
from zeep.exceptions import XMLParseError
from zeep.ns import XSD
def qname_attr(node, attr_name, target_namespace=None):
value = node.get(attr_name)
@ -9,11 +13,25 @@ def qname_attr(node, attr_name, target_namespace=None):
return as_qname(value, node.nsmap, target_namespace)
def as_qname(value, nsmap, target_namespace):
def as_qname(value, nsmap, target_namespace=None):
"""Convert the given value to a QName"""
if ':' in value:
prefix, local = value.split(':')
namespace = nsmap.get(prefix, prefix)
# The xml: prefix is always bound to the XML namespace, see
# https://www.w3.org/TR/xml-names/
if prefix == 'xml':
namespace = 'http://www.w3.org/XML/1998/namespace'
else:
namespace = nsmap.get(prefix)
if not namespace:
raise XMLParseError("No namespace defined for %r" % prefix)
# Workaround for https://github.com/mvantellingen/python-zeep/issues/349
if not local:
return etree.QName(XSD, 'anyType')
return etree.QName(namespace, local)
if target_namespace:
@ -61,3 +79,9 @@ def get_base_class(objects):
def detect_soap_env(envelope):
root_tag = etree.QName(envelope)
return root_tag.namespace
def get_media_type(value):
"""Parse a HTTP content-type header and return the media-type"""
main_value, parameters = cgi.parse_header(value)
return main_value

View File

@ -37,7 +37,5 @@ class WsAddressingPlugin(Plugin):
keep_ns_prefixes=header.nsmap,
top_nsmap=self.nsmap)
else:
etree.cleanup_namespaces(
header,
keep_ns_prefixes=header.nsmap)
etree.cleanup_namespaces(header)
return envelope, http_headers

View File

@ -1 +1,16 @@
"""
zeep.wsdl
---------
The wsdl module is responsible for parsing the WSDL document. This includes
the bindings and messages.
The structure and naming of the modules and classses closely follows the
WSDL 1.1 specification.
The serialization and deserialization of the SOAP/HTTP messages is done
by the zeep.wsdl.messages modules.
"""
from zeep.wsdl.wsdl import Document # noqa

View File

@ -3,8 +3,8 @@
See https://www.w3.org/TR/SOAP-attachments
"""
import base64
from io import BytesIO
from cached_property import cached_property
from requests.structures import CaseInsensitiveDict
@ -27,9 +27,21 @@ class MessagePack(object):
@cached_property
def attachments(self):
"""Return a list of attachments.
:rtype: list of Attachment
"""
return [Attachment(part) for part in self._parts]
def get_by_content_id(self, content_id):
"""get_by_content_id
:param content_id: The content-id to return
:type content_id: str
:rtype: Attachment
"""
for attachment in self.attachments:
if attachment.content_id == content_id:
return attachment
@ -37,9 +49,9 @@ class MessagePack(object):
class Attachment(object):
def __init__(self, part):
encoding = part.encoding or 'utf-8'
self.headers = CaseInsensitiveDict({
k.decode(part.encoding): v.decode(part.encoding)
k.decode(encoding): v.decode(encoding)
for k, v in part.headers.items()
})
self.content_type = self.headers.get('Content-Type', None)
@ -52,6 +64,11 @@ class Attachment(object):
@cached_property
def content(self):
"""Return the content of the attachment
:rtype: bytes or str
"""
encoding = self.headers.get('Content-Transfer-Encoding', None)
content = self._part.content

View File

@ -5,8 +5,8 @@ from requests_toolbelt.multipart.decoder import MultipartDecoder
from zeep import ns, 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.loader import parse_xml
from zeep.utils import as_qname, get_media_type, qname_attr
from zeep.wsdl.attachments import MessagePack
from zeep.wsdl.definitions import Binding, Operation
from zeep.wsdl.messages import DocumentMessage, RpcMessage
@ -97,9 +97,9 @@ class SoapBinding(Binding):
: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
:param args: The args to pass to the operation
:type args: tuple
:param kwargs: The **kwargs to pass to the operation
:param kwargs: The kwargs to pass to the operation
:type kwargs: dict
"""
@ -112,6 +112,11 @@ class SoapBinding(Binding):
options['address'], envelope, http_headers)
operation_obj = self.get(operation)
# If the client wants to return the raw data then let's do that.
if client.raw_response:
return response
return self.process_reply(client, operation_obj, response)
def process_reply(self, client, operation, response):
@ -131,20 +136,27 @@ class SoapBinding(Binding):
% response.status_code)
content_type = response.headers.get('Content-Type', 'text/xml')
if 'multipart/related' in content_type:
decoder = MultipartDecoder(response.content, content_type, 'utf-8')
media_type = get_media_type(content_type)
message_pack = None
if media_type == 'multipart/related':
decoder = MultipartDecoder(
response.content, content_type, response.encoding or 'utf-8')
content = decoder.parts[0].content
if len(decoder.parts) > 1:
message_pack = MessagePack(parts=decoder.parts[1:])
else:
content = response.content
message_pack = None
try:
doc = parse_xml(content)
doc = parse_xml(
content, self.transport,
strict=client.wsdl.strict,
xml_huge_tree=client.xml_huge_tree)
except XMLSyntaxError:
raise TransportError(
u'Server returned HTTP status %d (%s)'
'Server returned HTTP status %d (%s)'
% (response.status_code, response.content))
if client.wsse:
@ -187,6 +199,9 @@ class SoapBinding(Binding):
@classmethod
def parse(cls, definitions, xmlelement):
"""
Definition::
<wsdl:binding name="nmtoken" type="qname"> *
<-- extensibility element (1) --> *
<wsdl:operation name="nmtoken"> *
@ -210,7 +225,13 @@ class SoapBinding(Binding):
# 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':
supported_transports = [
'http://schemas.xmlsoap.org/soap/http',
'http://www.w3.org/2003/05/soap/bindings/HTTP/',
]
if transport not in supported_transports:
raise NotImplementedError(
"The binding transport %s is not supported (only soap/http)" % (
transport))
@ -328,12 +349,15 @@ class SoapOperation(Operation):
"{%s}Envelope root element. The root element found is %s "
) % (envelope_qname.namespace, envelope.tag))
return self.output.deserialize(envelope)
if self.output:
return self.output.deserialize(envelope)
@classmethod
def parse(cls, definitions, xmlelement, binding, nsmap):
"""
Definition::
<wsdl:operation name="nmtoken"> *
<soap:operation soapAction="uri"? style="rpc|document"?>?
<wsdl:input name="nmtoken"? > ?

View File

@ -1,7 +1,27 @@
"""
zeep.wsdl.definitions
~~~~~~~~~~~~~~~~~~~~~
A WSDL document exists out of a number of definitions. There are 6 major
definitions, these are:
- types
- message
- portType
- binding
- port
- service
This module defines the definitions which occur within a WSDL document,
"""
import warnings
from collections import OrderedDict, namedtuple
from six import python_2_unicode_compatible
from zeep.exceptions import IncompleteOperation
MessagePart = namedtuple('MessagePart', ['element', 'type'])
@ -13,8 +33,8 @@ class AbstractMessage(object):
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.
- element: Refers to an XSD element using a QName.
- type: Refers to an XSD simpleType or complexType using a QName.
"""
def __init__(self, name):
@ -72,6 +92,8 @@ class PortType(object):
class Binding(object):
"""Base class for the various bindings (SoapBinding / HttpBinding)
.. raw:: ascii
Binding
|
+-> Operation
@ -100,8 +122,13 @@ class Binding(object):
def resolve(self, definitions):
self.port_type = definitions.get('port_types', self.port_name.text)
for operation in self._operations.values():
operation.resolve(definitions)
for name, operation in list(self._operations.items()):
try:
operation.resolve(definitions)
except IncompleteOperation as exc:
warnings.warn(str(exc))
del self._operations[name]
def _operation_add(self, operation):
# XXX: operation name is not unique
@ -146,7 +173,12 @@ class Operation(object):
self.faults = {}
def resolve(self, definitions):
self.abstract = self.binding.port_type.operations[self.name]
try:
self.abstract = self.binding.port_type.operations[self.name]
except KeyError:
raise IncompleteOperation(
"The wsdl:operation %r was not found in the wsdl:portType %r" % (
self.name, self.binding.port_type.name.text))
def __repr__(self):
return '<%s(name=%r, style=%r)>' % (
@ -170,6 +202,9 @@ class Operation(object):
@classmethod
def parse(cls, wsdl, xmlelement, binding):
"""
Definition::
<wsdl:operation name="nmtoken"> *
<-- extensibility element (2) --> *
<wsdl:input name="nmtoken"? > ?
@ -182,12 +217,17 @@ class Operation(object):
<-- extensibility element (5) --> *
</wsdl:fault>
</wsdl:operation>
"""
raise NotImplementedError()
@python_2_unicode_compatible
class Port(object):
"""Specifies an address for a binding, thus defining a single communication
endpoint.
"""
def __init__(self, name, binding_name, xmlelement):
self.name = name
self._resolve_context = {
@ -231,7 +271,9 @@ class Port(object):
@python_2_unicode_compatible
class Service(object):
"""Used to aggregate a set of related ports.
"""
def __init__(self, name):
self.ports = OrderedDict()
self.name = name

View File

@ -1,3 +1,20 @@
"""
zeep.wsdl.messages
~~~~~~~~~~~~~~~~~~
The messages are responsible for serializing and deserializing
.. inheritance-diagram::
zeep.wsdl.messages.soap.DocumentMessage
zeep.wsdl.messages.soap.RpcMessage
zeep.wsdl.messages.http.UrlEncoded
zeep.wsdl.messages.http.UrlReplacement
zeep.wsdl.messages.mime.MimeContent
zeep.wsdl.messages.mime.MimeXML
zeep.wsdl.messages.mime.MimeMultipart
:parts: 1
"""
from .http import * # noqa
from .mime import * # noqa
from .soap import * # noqa

View File

@ -1,3 +1,8 @@
"""
zeep.wsdl.messages.base
~~~~~~~~~~~~~~~~~~~~~~~
"""
from collections import namedtuple
from zeep import xsd
@ -31,15 +36,17 @@ class ConcreteMessage(object):
if isinstance(self.body.type, xsd.ComplexType):
try:
if len(self.body.type.elements) == 1:
return self.body.type.elements[0][1].type.signature()
return self.body.type.elements[0][1].type.signature(
schema=self.wsdl.types, standalone=False)
except AttributeError:
return None
return self.body.type.signature()
return self.body.type.signature(schema=self.wsdl.types, standalone=False)
parts = [self.body.type.signature()]
parts = [self.body.type.signature(schema=self.wsdl.types, standalone=False)]
if getattr(self, 'header', None):
parts.append('_soapheaders={%s}' % self.header.signature())
parts.append('_soapheaders={%s}' % self.header.signature(
schema=self.wsdl.types), standalone=False)
return ', '.join(part for part in parts if part)
@classmethod

View File

@ -1,3 +1,8 @@
"""
zeep.wsdl.messages.http
~~~~~~~~~~~~~~~~~~~~~~~
"""
from zeep import xsd
from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage

View File

@ -1,3 +1,8 @@
"""
zeep.wsdl.messages.mime
~~~~~~~~~~~~~~~~~~~~~~~
"""
import six
from defusedxml.lxml import fromstring
from lxml import etree
@ -79,6 +84,14 @@ class MimeContent(MimeMessage):
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.
:param wsdl: The main wsdl document
:type wsdl: zeep.wsdl.wsdl.Document
:param name:
:param operation: The operation to which this message belongs
:type operation: zeep.wsdl.bindings.soap.SoapOperation
:param part_name:
:type type: str
"""
def __init__(self, wsdl, name, operation, content_type, part_name):
super(MimeContent, self).__init__(wsdl, name, operation, part_name)
@ -131,6 +144,14 @@ class MimeXML(MimeMessage):
only a single part. The part references a concrete schema using the element
attribute for simple parts or type attribute for composite parts
:param wsdl: The main wsdl document
:type wsdl: zeep.wsdl.wsdl.Document
:param name:
:param operation: The operation to which this message belongs
:type operation: zeep.wsdl.bindings.soap.SoapOperation
:param part_name:
:type type: str
"""
def serialize(self, *args, **kwargs):
raise NotImplementedError()
@ -170,5 +191,13 @@ class MimeMultipart(MimeMessage):
the part. If more than one MIME element appears inside a mime:part, they
are alternatives.
:param wsdl: The main wsdl document
:type wsdl: zeep.wsdl.wsdl.Document
:param name:
:param operation: The operation to which this message belongs
:type operation: zeep.wsdl.bindings.soap.SoapOperation
:param part_name:
:type type: str
"""
pass

View File

@ -0,0 +1,81 @@
import copy
from lxml import etree
def process_multiref(node):
"""Iterate through the tree and replace the referened elements.
This method replaces the nodes with an href attribute and replaces it
with the elements it's referencing to (which have an id attribute).abs
"""
multiref_objects = {
elm.attrib['id']: elm for elm in node.xpath('*[@id]')
}
if not multiref_objects:
return
used_nodes = []
def process(node):
# TODO (In Soap 1.2 this is 'ref')
href = node.attrib.get('href')
if href and href.startswith('#'):
obj = multiref_objects.get(href[1:])
if obj is not None:
used_nodes.append(obj)
parent = node.getparent()
new = _dereference_element(obj, node)
# Replace the node with the new dereferenced node
parent.insert(parent.index(node), new)
parent.remove(node)
node = new
for child in node:
process(child)
process(node)
# Remove the old dereferenced nodes from the tree
for node in used_nodes:
parent = node.getparent()
if parent is not None:
parent.remove(node)
def _dereference_element(source, target):
reverse_nsmap = {v: k for k, v in target.nsmap.items()}
specific_nsmap = {k: v for k, v in source.nsmap.items() if k not in target.nsmap}
new = etree.Element(target.tag, nsmap=specific_nsmap)
# Copy the attributes. This is actually the difficult part since the
# namespace prefixes can change in the attribute values. So for example
# the xsi:type="ns11:my-type" need's to be parsed to use a new global
# prefix.
for key, value in source.attrib.items():
if key == 'id':
continue
setted = False
if value.count(':') == 1:
prefix, localname = value.split(':')
if prefix in specific_nsmap:
namespace = specific_nsmap[prefix]
if namespace in reverse_nsmap:
new.set(key, '%s:%s' % (reverse_nsmap[namespace], localname))
setted = True
if not setted:
new.set(key, value)
# Copy the children and the text content
for child in source:
new.append(copy.deepcopy(child))
new.text = source.text
return new

View File

@ -1,12 +1,19 @@
"""
zeep.wsdl.messages.soap
~~~~~~~~~~~~~~~~~~~~~~~
"""
import copy
from collections import OrderedDict
from lxml import etree
from lxml.builder import ElementMaker
from zeep import ns
from zeep import exceptions, xsd
from zeep.utils import as_qname
from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage
from zeep.wsdl.messages.multiref import process_multiref
__all__ = [
'DocumentMessage',
@ -15,8 +22,19 @@ __all__ = [
class SoapMessage(ConcreteMessage):
"""Base class for the SOAP Document and RPC messages"""
"""Base class for the SOAP Document and RPC messages
:param wsdl: The main wsdl document
:type wsdl: zeep.wsdl.Document
:param name:
:param operation: The operation to which this message belongs
:type operation: zeep.wsdl.bindings.soap.SoapOperation
:param type: 'input' or 'output'
:type type: str
:param nsmap: The namespace mapping
:type nsmap: dict
"""
def __init__(self, wsdl, name, operation, type, nsmap):
super(SoapMessage, self).__init__(wsdl, name, operation)
self.nsmap = nsmap
@ -71,6 +89,7 @@ class SoapMessage(ConcreteMessage):
if not self.envelope:
return None
body = envelope.find('soap-env:Body', namespaces=self.nsmap)
body_result = self._deserialize_body(body)
@ -96,7 +115,8 @@ class SoapMessage(ConcreteMessage):
result = next(iter(result.__values__.values()))
if isinstance(result, xsd.CompoundValue):
children = result._xsd_type.elements
if len(children) == 1:
attributes = result._xsd_type.attributes
if len(children) == 1 and len(attributes) == 0:
item_name, item_element = children[0]
retval = getattr(result, item_name)
return retval
@ -110,14 +130,16 @@ class SoapMessage(ConcreteMessage):
if isinstance(self.envelope.type, xsd.ComplexType):
try:
if len(self.envelope.type.elements) == 1:
return self.envelope.type.elements[0][1].type.signature()
return self.envelope.type.elements[0][1].type.signature(
schema=self.wsdl.types, standalone=False)
except AttributeError:
return None
return self.envelope.type.signature()
return self.envelope.type.signature(schema=self.wsdl.types, standalone=False)
parts = [self.body.type.signature()]
parts = [self.body.type.signature(schema=self.wsdl.types, standalone=False)]
if self.header.type._element:
parts.append('_soapheaders={%s}' % self.header.signature())
parts.append('_soapheaders={%s}' % self.header.type.signature(
schema=self.wsdl.types, standalone=False))
return ', '.join(part for part in parts if part)
@classmethod
@ -156,6 +178,8 @@ class SoapMessage(ConcreteMessage):
body_data = None
header_data = None
# After some profiling it turns out that .find() and .findall() in this
# case are twice as fast as the xpath method
body = xmlelement.find('soap:body', namespaces=operation.binding.nsmap)
if body is not None:
body_data = cls._parse_body(body)
@ -270,15 +294,16 @@ class SoapMessage(ConcreteMessage):
elements from the body and the headers.
"""
all_elements = xsd.Sequence([
xsd.Element('body', self.body.type),
])
all_elements = xsd.Sequence([])
if self.header.type._element:
all_elements.append(
xsd.Element('header', self.header.type))
xsd.Element('{%s}header' % self.nsmap['soap-env'], self.header.type))
return xsd.Element('envelope', xsd.ComplexType(all_elements))
all_elements.append(
xsd.Element('{%s}body' % self.nsmap['soap-env'], self.body.type))
return xsd.Element('{%s}envelope' % self.nsmap['soap-env'], xsd.ComplexType(all_elements))
def _serialize_header(self, headers_value, nsmap):
if not headers_value:
@ -327,9 +352,9 @@ class SoapMessage(ConcreteMessage):
def _resolve_header(self, info, definitions, parts):
name = etree.QName(self.nsmap['soap-env'], 'Header')
sequence = xsd.Sequence()
container = xsd.All(consume_other=True)
if not info:
return xsd.Element(name, xsd.ComplexType(sequence))
return xsd.Element(name, xsd.ComplexType(container))
for item in info:
message_name = item['message'].text
@ -345,14 +370,28 @@ class SoapMessage(ConcreteMessage):
element.attr_name = part_name
else:
element = xsd.Element(part_name, part.type)
sequence.append(element)
return xsd.Element(name, xsd.ComplexType(sequence))
container.append(element)
return xsd.Element(name, xsd.ComplexType(container))
class DocumentMessage(SoapMessage):
"""In the document message there are no additional wrappers, and the
message parts appear directly under the SOAP Body element.
.. inheritance-diagram:: zeep.wsdl.messages.soap.DocumentMessage
:parts: 1
:param wsdl: The main wsdl document
:type wsdl: zeep.wsdl.Document
:param name:
:param operation: The operation to which this message belongs
:type operation: zeep.wsdl.bindings.soap.SoapOperation
:param type: 'input' or 'output'
:type type: str
:param nsmap: The namespace mapping
:type nsmap: dict
"""
def __init__(self, *args, **kwargs):
@ -407,6 +446,20 @@ class RpcMessage(SoapMessage):
identically to the corresponding parameter of the call. Parts are arranged
in the same order as the parameters of the call.
.. inheritance-diagram:: zeep.wsdl.messages.soap.DocumentMessage
:parts: 1
:param wsdl: The main wsdl document
:type wsdl: zeep.wsdl.Document
:param name:
:param operation: The operation to which this message belongs
:type operation: zeep.wsdl.bindings.soap.SoapOperation
:param type: 'input' or 'output'
:type type: str
:param nsmap: The namespace mapping
:type nsmap: dict
"""
def _resolve_body(self, info, definitions, parts):
@ -444,6 +497,8 @@ class RpcMessage(SoapMessage):
element.
"""
process_multiref(body_element)
response_element = body_element.getchildren()[0]
if self.body:
result = self.body.parse(response_element, self.wsdl.types)

View File

@ -1,5 +1,11 @@
"""
zeep.wsdl.parse
~~~~~~~~~~~~~~~
"""
from lxml import etree
from zeep.exceptions import IncompleteMessage, LookupError, NamespaceError
from zeep.utils import qname_attr
from zeep.wsdl import definitions
@ -12,11 +18,20 @@ NSMAP = {
def parse_abstract_message(wsdl, xmlelement):
"""Create an AbstractMessage object from a xml element.
Definition::
<definitions .... >
<message name="nmtoken"> *
<part name="nmtoken" element="qname"? type="qname"?/> *
</message>
</definitions>
:param wsdl: The parent definition instance
:type wsdl: zeep.wsdl.wsdl.Definition
:param xmlelement: The XML node
:type xmlelement: lxml.etree._Element
:rtype: zeep.wsdl.definitions.AbstractMessage
"""
tns = wsdl.target_namespace
parts = []
@ -26,10 +41,17 @@ def parse_abstract_message(wsdl, xmlelement):
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)
try:
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)
except (NamespaceError, LookupError):
raise IncompleteMessage((
"The wsdl:message for %r contains "
"invalid xsd types or elements"
) % part_name)
part = definitions.MessagePart(part_element, part_type)
parts.append((part_name, part))
@ -48,6 +70,8 @@ def parse_abstract_operation(wsdl, xmlelement):
This is called from the parse_port_type function since the abstract
operations are part of the port type element.
Definition::
<wsdl:operation name="nmtoken">*
<wsdl:documentation .... /> ?
<wsdl:input name="nmtoken"? message="qname">?
@ -61,6 +85,12 @@ def parse_abstract_operation(wsdl, xmlelement):
</wsdl:fault>
</wsdl:operation>
:param wsdl: The parent definition instance
:type wsdl: zeep.wsdl.wsdl.Definition
:param xmlelement: The XML node
:type xmlelement: lxml.etree._Element
:rtype: zeep.wsdl.definitions.AbstractOperation
"""
name = xmlelement.get('name')
kwargs = {
@ -75,7 +105,11 @@ def parse_abstract_operation(wsdl, xmlelement):
param_msg = qname_attr(
msg_node, 'message', wsdl.target_namespace)
param_name = msg_node.get('name')
param_value = wsdl.get('messages', param_msg.text)
try:
param_value = wsdl.get('messages', param_msg.text)
except IndexError:
return
if tag_name == 'input':
kwargs['input_message'] = param_value
@ -95,18 +129,27 @@ def parse_abstract_operation(wsdl, xmlelement):
def parse_port_type(wsdl, xmlelement):
"""Create a PortType object from a xml element.
Definition::
<wsdl:definitions .... >
<wsdl:portType name="nmtoken">
<wsdl:operation name="nmtoken" .... /> *
</wsdl:portType>
</wsdl:definitions>
:param wsdl: The parent definition instance
:type wsdl: zeep.wsdl.wsdl.Definition
:param xmlelement: The XML node
:type xmlelement: lxml.etree._Element
:rtype: zeep.wsdl.definitions.PortType
"""
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
if operation:
operations[operation.name] = operation
return definitions.PortType(name, operations)
@ -116,11 +159,19 @@ def parse_port(wsdl, xmlelement):
This is called via the parse_service function since ports are part of the
service xml elements.
Definition::
<wsdl:port name="nmtoken" binding="qname"> *
<wsdl:documentation .... /> ?
<-- extensibility element -->
</wsdl:port>
:param wsdl: The parent definition instance
:type wsdl: zeep.wsdl.wsdl.Definition
:param xmlelement: The XML node
:type xmlelement: lxml.etree._Element
:rtype: zeep.wsdl.definitions.Port
"""
name = xmlelement.get('name')
binding_name = qname_attr(xmlelement, 'binding', wsdl.target_namespace)
@ -130,7 +181,7 @@ def parse_port(wsdl, xmlelement):
def parse_service(wsdl, xmlelement):
"""
Syntax::
Definition::
<wsdl:service name="nmtoken"> *
<wsdl:documentation .... />?
@ -150,6 +201,12 @@ def parse_service(wsdl, xmlelement):
</port>
</service>
:param wsdl: The parent definition instance
:type wsdl: zeep.wsdl.wsdl.Definition
:param xmlelement: The XML node
:type xmlelement: lxml.etree._Element
:rtype: zeep.wsdl.definitions.Service
"""
name = xmlelement.get('name')
ports = []

View File

@ -1,3 +1,8 @@
"""
zeep.wsdl.utils
~~~~~~~~~~~~~~~
"""
from lxml import etree
from six.moves.urllib.parse import urlparse, urlunparse

View File

@ -1,15 +1,21 @@
"""
zeep.wsdl.wsdl
~~~~~~~~~~~~~~
"""
from __future__ import print_function
import logging
import operator
import os
import warnings
from collections import OrderedDict
import six
from lxml import etree
from zeep.parser import (
absolute_location, is_relative_path, load_external, parse_xml)
from zeep.exceptions import IncompleteMessage
from zeep.loader import absolute_location, is_relative_path, load_external
from zeep.utils import findall_multiple_ns
from zeep.wsdl import parse
from zeep.xsd import Schema
@ -34,18 +40,23 @@ class Document(object):
resolves references which were not yet available during the initial
parsing phase.
:param location: Location of this WSDL
:type location: string
:param transport: The transport object to be used
:type transport: zeep.transports.Transport
:param base: The base location of this document
:type base: str
:param strict: Indicates if strict mode is enabled
:type strict: bool
"""
def __init__(self, location, transport, base=None):
def __init__(self, location, transport, base=None, strict=True):
"""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
"""
if isinstance(location, six.string_types):
if is_relative_path(location):
@ -55,12 +66,17 @@ class Document(object):
self.location = base
self.transport = transport
self.strict = strict
# Dict with all definition objects within this WSDL
self._definitions = {}
self.types = Schema([], transport=self.transport, location=self.location)
self.types = Schema(
node=None,
transport=self.transport,
location=self.location,
strict=self.strict)
document = self._load_content(location)
document = self._get_xml_document(location)
root_definitions = Definition(self, document, self.location)
root_definitions.resolve_imports()
@ -75,8 +91,6 @@ class Document(object):
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():
@ -84,18 +98,14 @@ class Document(object):
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)
for elm_obj in sorted(self.types.elements, key=lambda k: k.qname):
value = elm_obj.signature(schema=self.types)
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)
value = type_obj.signature(schema=self.types)
print(' ' * 4, value)
print('')
@ -118,7 +128,7 @@ class Document(object):
print('%s%s' % (' ' * 12, six.text_type(operation)))
print('')
def _load_content(self, location):
def _get_xml_document(self, location):
"""Load the XML content from the given location and return an
lxml.Element object.
@ -126,9 +136,8 @@ class Document(object):
:type location: string
"""
if hasattr(location, 'read'):
return parse_xml(location.read())
return load_external(location, self.transport, self.location)
return load_external(
location, self.transport, self.location, strict=self.strict)
def _add_definition(self, definition):
key = (definition.target_namespace, definition.location)
@ -136,9 +145,18 @@ class Document(object):
class Definition(object):
"""The Definition represents one wsdl:definition within a Document."""
"""The Definition represents one wsdl:definition within a Document.
:param wsdl: The wsdl
"""
def __init__(self, wsdl, doc, location):
"""fo
:param wsdl: The wsdl
"""
logger.debug("Creating definition for %s", location)
self.wsdl = wsdl
self.location = location
@ -183,7 +201,16 @@ class Definition(object):
try:
return definition.get(name, key, _processed)
except IndexError:
pass
# Try to see if there is an item which has no namespace
# but where the localname matches. This is basically for
# #356 but in the future we should also ignore mismatching
# namespaces as last fallback
fallback_key = etree.QName(key).localname
try:
return definition.get(name, fallback_key, _processed)
except IndexError:
pass
raise IndexError("No definition %r in %r found" % (key, name))
def resolve_imports(self):
@ -234,7 +261,7 @@ class Definition(object):
if key in self.wsdl._definitions:
self.imports[key] = self.wsdl._definitions[key]
else:
document = self.wsdl._load_content(location)
document = self.wsdl._get_xml_document(location)
if etree.QName(document.tag).localname == 'schema':
self.types.add_documents([document], location)
else:
@ -251,6 +278,8 @@ class Definition(object):
If the wsdl:types doesn't container an xml schema then an empty schema
is returned instead.
Definition::
<definitions .... >
<types>
<xsd:schema .... />*
@ -279,6 +308,9 @@ class Definition(object):
def parse_messages(self, doc):
"""
Definition::
<definitions .... >
<message name="nmtoken"> *
<part name="nmtoken" element="qname"? type="qname"?/> *
@ -291,14 +323,20 @@ class Definition(object):
"""
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)
try:
msg = parse.parse_abstract_message(self, msg_node)
except IncompleteMessage as exc:
warnings.warn(str(exc))
else:
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
Definition::
<wsdl:definitions .... >
<wsdl:portType name="nmtoken">
<wsdl:operation name="nmtoken" .... /> *
@ -323,7 +361,7 @@ class Definition(object):
HTTP Post. The detection of the type of bindings is done by the
bindings themselves using the introspection of the xml nodes.
XML Structure::
Definition::
<wsdl:definitions .... >
<wsdl:binding name="nmtoken" type="qname"> *
@ -345,6 +383,9 @@ class Definition(object):
:param doc: The source document
:type doc: lxml.etree._Element
:returns: Dictionary with binding name as key and Binding instance as
value
:rtype: dict
"""
result = {}
@ -382,6 +423,9 @@ class Definition(object):
def parse_service(self, doc):
"""
Definition::
<wsdl:definitions .... >
<wsdl:service .... > *
<wsdl:port name="nmtoken" binding="qname"> *

View File

@ -11,38 +11,63 @@ module.
from lxml import etree
from lxml.etree import QName
from zeep import ns
from zeep.exceptions import SignatureVerificationFailed
from zeep.utils import detect_soap_env
from zeep.wsse.utils import ensure_id, get_security_header
try:
import xmlsec
except ImportError:
xmlsec = None
from zeep import ns
from zeep.utils import detect_soap_env
from zeep.exceptions import SignatureVerificationFailed
from zeep.wsse.utils import ensure_id, get_security_header
# SOAP envelope
SOAP_NS = 'http://schemas.xmlsoap.org/soap/envelope/'
def _read_file(f_name):
with open(f_name, "rb") as f:
return f.read()
class Signature(object):
def _make_sign_key(key_data, cert_data, password):
key = xmlsec.Key.from_memory(key_data,
xmlsec.KeyFormat.PEM, password)
key.load_cert_from_memory(cert_data,
xmlsec.KeyFormat.PEM)
return key
def _make_verify_key(cert_data):
key = xmlsec.Key.from_memory(cert_data,
xmlsec.KeyFormat.CERT_PEM, None)
return key
class MemorySignature(object):
"""Sign given SOAP envelope with WSSE sig using given key and cert."""
def __init__(self, key_file, certfile, password=None):
def __init__(self, key_data, cert_data, password=None):
check_xmlsec_import()
self.key_file = key_file
self.certfile = certfile
self.key_data = key_data
self.cert_data = cert_data
self.password = password
def apply(self, envelope, headers):
sign_envelope(envelope, self.key_file, self.certfile, self.password)
key = _make_sign_key(self.key_data, self.cert_data, self.password)
_sign_envelope_with_key(envelope, key)
return envelope, headers
def verify(self, envelope):
verify_envelope(envelope, self.certfile)
key = _make_verify_key(self.cert_data)
_verify_envelope_with_key(envelope, key)
return envelope
class Signature(MemorySignature):
"""Sign given SOAP envelope with WSSE sig using given key file and cert file."""
def __init__(self, key_file, certfile, password=None):
super(Signature, self).__init__(_read_file(key_file),
_read_file(certfile),
password)
def check_xmlsec_import():
if xmlsec is None:
@ -141,6 +166,12 @@ def sign_envelope(envelope, keyfile, certfile, password=None):
</soap:Envelope>
"""
# Load the signing key and certificate.
key = _make_sign_key(_read_file(keyfile), _read_file(certfile), password)
return _sign_envelope_with_key(envelope, key)
def _sign_envelope_with_key(envelope, key):
# Create the Signature node.
signature = xmlsec.template.create(
envelope,
@ -155,10 +186,6 @@ def sign_envelope(envelope, keyfile, certfile, password=None):
xmlsec.template.x509_data_add_issuer_serial(x509_data)
xmlsec.template.x509_data_add_certificate(x509_data)
# Load the signing key and certificate.
key = xmlsec.Key.from_file(keyfile, xmlsec.KeyFormat.PEM, password=password)
key.load_cert_from_file(certfile, xmlsec.KeyFormat.PEM)
# Insert the Signature node in the wsse:Security header.
security = get_security_header(envelope)
security.insert(0, signature)
@ -190,12 +217,19 @@ def verify_envelope(envelope, certfile):
Expects a document like that found in the sample XML in the ``sign()``
docstring.
Raise SignatureValidationFailed on failure, silent on success.
Raise SignatureVerificationFailed on failure, silent on success.
"""
key = _make_verify_key(_read_file(certfile))
return _verify_envelope_with_key(envelope, key)
def _verify_envelope_with_key(envelope, key):
soap_env = detect_soap_env(envelope)
header = envelope.find(QName(soap_env, 'Header'))
if not header:
raise SignatureVerificationFailed()
security = header.find(QName(ns.WSSE, 'Security'))
signature = security.find(QName(ns.DS, 'Signature'))
@ -213,7 +247,6 @@ def verify_envelope(envelope, certfile):
)[0]
ctx.register_id(referenced, 'Id', ns.WSU)
key = xmlsec.Key.from_file(certfile, xmlsec.KeyFormat.CERT_PEM, None)
ctx.key = key
try:

View File

@ -1,8 +1,8 @@
from uuid import uuid4
from lxml import etree
import datetime
from uuid import uuid4
import pytz
from lxml import etree
from lxml.builder import ElementMaker
from zeep import ns

View File

@ -1,4 +1,9 @@
from zeep.xsd.const import SkipValue # noqa
"""
zeep.xsd
--------
"""
from zeep.xsd.const import Nil, SkipValue # noqa
from zeep.xsd.elements import * # noqa
from zeep.xsd.schema import Schema # noqa
from zeep.xsd.types import * # noqa

View File

@ -1,15 +1,13 @@
from lxml import etree
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
NS_XSD = 'http://www.w3.org/2001/XMLSchema'
from zeep import ns
def xsi_ns(localname):
return etree.QName(NS_XSI, localname)
return etree.QName(ns.XSI, localname)
def xsd_ns(localname):
return etree.QName(NS_XSD, localname)
return etree.QName(ns.XSD, localname)
class _StaticIdentity(object):
@ -22,3 +20,4 @@ class _StaticIdentity(object):
NotSet = _StaticIdentity('NotSet')
SkipValue = _StaticIdentity('SkipValue')
Nil = _StaticIdentity('Nil')

View File

@ -2,9 +2,9 @@ import logging
from lxml import etree
from zeep import exceptions
from zeep import exceptions, ns
from zeep.utils import qname_attr
from zeep.xsd.const import xsi_ns, NotSet
from zeep.xsd.const import NotSet, xsi_ns
from zeep.xsd.elements.base import Base
from zeep.xsd.utils import max_occurs_iter
from zeep.xsd.valueobjects import AnyObject
@ -84,7 +84,19 @@ class Any(Base):
return {}
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
"""Consume matching xmlelements and call parse() on each of them"""
"""Consume matching xmlelements and call parse() on each of them
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:return: dict or None
"""
result = []
for _unused in max_occurs_iter(self.max_occurs):
@ -169,9 +181,10 @@ class Any(Base):
# 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
expected_types = (etree._Element, dict,) + self.restrict.accepted_types
else:
expected_types = (etree._Element, AnyObject)
expected_types = (etree._Element, dict,AnyObject)
if not isinstance(value, expected_types):
type_names = [
'%s.%s' % (t.__module__, t.__name__) for t in expected_types
@ -188,7 +201,7 @@ class Any(Base):
def resolve(self):
return self
def signature(self, depth=()):
def signature(self, schema=None, standalone=True):
if self.restrict:
base = self.restrict.name
else:
@ -201,23 +214,30 @@ class Any(Base):
class AnyAttribute(Base):
name = None
_ignore_attributes = [
etree.QName(ns.XSI, 'type')
]
def __init__(self, process_contents='strict'):
self.qname = None
self.process_contents = process_contents
def parse(self, attributes, context=None):
return attributes
result = {}
for key, value in attributes.items():
if key not in self._ignore_attributes:
result[key] = value
return result
def resolve(self):
return self
def render(self, parent, value, render_path=None):
if value is None:
if value in (None, NotSet):
return
for name, val in value.items():
parent.set(name, val)
def signature(self, depth=()):
def signature(self, schema=None, standalone=True):
return '{}'

View File

@ -88,5 +88,5 @@ class AttributeGroup(object):
self._attributes = resolved
return self
def signature(self, depth=()):
return ', '.join(attr.signature() for attr in self._attributes)
def signature(self, schema=None, standalone=True):
return ', '.join(attr.signature(schema) for attr in self._attributes)

View File

@ -25,8 +25,20 @@ class Base(object):
raise NotImplementedError()
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
"""Consume matching xmlelements and call parse() on each of them"""
"""Consume matching xmlelements and call parse() on each of them
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:return: dict or None
"""
raise NotImplementedError()
def signature(self, depth=()):
def signature(self, schema=None, standalone=False):
return ''

View File

@ -35,6 +35,6 @@ class Schema(Base):
return self
default_elements = {
xsd_ns('schema'): Schema(),
}
_elements = [
Schema
]

View File

@ -6,10 +6,10 @@ from lxml import etree
from zeep import exceptions
from zeep.exceptions import UnexpectedElementError
from zeep.utils import qname_attr
from zeep.xsd.const import NotSet, xsi_ns
from zeep.xsd.const import Nil, NotSet, xsi_ns
from zeep.xsd.context import XmlParserContext
from zeep.xsd.elements.base import Base
from zeep.xsd.utils import max_occurs_iter
from zeep.xsd.utils import create_prefixed_name, max_occurs_iter
logger = logging.getLogger(__name__)
@ -38,7 +38,10 @@ class Element(Base):
def __str__(self):
if self.type:
return '%s(%s)' % (self.name, self.type.signature())
if self.type.is_global:
return '%s(%s)' % (self.name, self.type.qname)
else:
return '%s(%s)' % (self.name, self.type.signature())
return '%s()' % self.name
def __call__(self, *args, **kwargs):
@ -57,10 +60,16 @@ class Element(Base):
self.__class__ == other.__class__ and
self.__dict__ == other.__dict__)
def get_prefixed_name(self, schema):
return create_prefixed_name(self.qname, schema)
@property
def default_value(self):
value = [] if self.accepts_multiple else self.default
return value
if self.accepts_multiple:
return []
if self.is_optional:
return None
return self.default
def clone(self, name=None, min_occurs=1, max_occurs=1):
new = copy.copy(self)
@ -81,6 +90,18 @@ class Element(Base):
use that for further processing. This should only be done for subtypes
of the defined type but for now we just accept everything.
This is the entrypoint for parsing an xml document.
:param xmlelement: The XML element to parse
:type xmlelements: lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param allow_none: Allow none
:type allow_none: bool
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:return: dict or None
"""
context = context or XmlParserContext()
instance_type = qname_attr(xmlelement, xsi_ns('type'))
@ -89,14 +110,27 @@ class Element(Base):
xsd_type = schema.get_type(instance_type, fail_silently=True)
xsd_type = xsd_type or self.type
return xsd_type.parse_xmlelement(
xmlelement, schema, allow_none=allow_none, context=context)
xmlelement, schema, allow_none=allow_none, context=context,
schema_type=self.type)
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"""
"""Consume matching xmlelements and call parse() on each of them
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:return: dict or None
"""
result = []
num_matches = 0
for _unused in max_occurs_iter(self.max_occurs):
@ -113,7 +147,8 @@ class Element(Base):
element_tag = etree.QName(xmlelements[0].tag)
if (
element_tag.namespace and self.qname.namespace and
element_tag.namespace != self.qname.namespace
element_tag.namespace != self.qname.namespace and
schema.strict
):
break
@ -123,8 +158,7 @@ class Element(Base):
num_matches += 1
item = self.parse(
xmlelement, schema, allow_none=True, context=context)
if item is not None:
result.append(item)
result.append(item)
else:
# If the element passed doesn't match and the current one is
# not optional then throw an error
@ -158,6 +192,12 @@ class Element(Base):
def _render_value_item(self, parent, value, render_path):
"""Render the value on the parent lxml.Element"""
if value is Nil:
elm = etree.SubElement(parent, self.qname)
elm.set(xsi_ns('nil'), 'true')
return
if value is None or value is NotSet:
if self.is_optional:
return
@ -215,11 +255,19 @@ class Element(Base):
self.resolve_type()
return self
def signature(self, depth=()):
if len(depth) > 0 and self.is_global:
return self.name + '()'
def signature(self, schema=None, standalone=True):
from zeep.xsd import ComplexType
if self.type.is_global or (not standalone and self.is_global):
value = self.type.get_prefixed_name(schema)
else:
value = self.type.signature(schema, standalone=False)
if not standalone and isinstance(self.type, ComplexType):
value = '{%s}' % value
if standalone:
value = '%s(%s)' % (self.get_prefixed_name(schema), value)
value = self.type.signature(depth)
if self.accepts_multiple:
return '%s[]' % value
return value

View File

@ -1,36 +1,56 @@
"""
zeep.xsd.elements.indicators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicators are a collection of elements. There are four available, these are
All, Choice, Group and Sequence.
Indicator -> OrderIndicator -> All
-> Choice
-> Sequence
-> Group
"""
import copy
import operator
from collections import OrderedDict, defaultdict, deque
from cached_property import threaded_cached_property
from zeep.exceptions import UnexpectedElementError
from zeep.exceptions import UnexpectedElementError, ValidationError
from zeep.xsd.const import NotSet, SkipValue
from zeep.xsd.elements import Any, Element
from zeep.xsd.elements.base import Base
from zeep.xsd.utils import (
NamePrefixGenerator, UniqueNameGenerator, max_occurs_iter)
NamePrefixGenerator, UniqueNameGenerator, create_prefixed_name,
max_occurs_iter)
__all__ = ['All', 'Choice', 'Group', 'Sequence']
class Indicator(Base):
"""Base class for the other indicators"""
def __repr__(self):
return '<%s(%s)>' % (
self.__class__.__name__, super(Indicator, self).__repr__())
@threaded_cached_property
@property
def default_value(self):
return OrderedDict([
values = OrderedDict([
(name, element.default_value) for name, element in self.elements
])
if self.accepts_multiple:
return {'_value_1': values}
return values
def clone(self, name, min_occurs=1, max_occurs=1):
raise NotImplementedError()
class OrderIndicator(Indicator, list):
"""Base class for All, Choice and Sequence classes."""
name = None
def __init__(self, elements=None, min_occurs=1, max_occurs=1):
@ -85,16 +105,30 @@ class OrderIndicator(Indicator, list):
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
if not self.accepts_multiple:
values = [values]
results = set()
for value in values:
num = 0
for name, element in self.elements_nested:
if isinstance(element, Element):
if element.name in value and value[element.name] is not None:
num += 1
else:
num += element.accept(value)
results.add(num)
return max(results)
def parse_args(self, args, index=0):
# If the sequence contains an choice element then we can't convert
# the args to kwargs since Choice elements don't work with position
# arguments
for name, elm in self.elements_nested:
if isinstance(elm, Choice):
raise TypeError("Choice elements only work with keyword arguments")
result = {}
for name, element in self.elements:
if index >= len(args):
@ -110,11 +144,24 @@ class OrderIndicator(Indicator, list):
The available_kwargs is modified in-place. Returns a dict with the
result.
:param kwargs: The kwargs
:type kwargs: dict
:param name: The name as which this type is registered in the parent
:type name: str
:param available_kwargs: The kwargs keys which are still available,
modified in place
:type available_kwargs: set
:rtype: dict
"""
if self.accepts_multiple:
assert name
if name and name in available_kwargs:
if name:
if name not in available_kwargs:
return {}
assert self.accepts_multiple
# Make sure we have a list, lame lame
item_kwargs = kwargs.get(name)
@ -123,19 +170,26 @@ class OrderIndicator(Indicator, list):
result = []
for item_value in max_occurs_iter(self.max_occurs, item_kwargs):
item_kwargs = set(item_value.keys())
try:
item_kwargs = set(item_value.keys())
except AttributeError:
raise TypeError(
"A list of dicts is expected for unbounded Sequences")
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)
if item_kwargs:
raise TypeError((
"%s() got an unexpected keyword argument %r."
) % (self, list(item_kwargs)[0]))
result.append(subresult)
if self.accepts_multiple:
result = {name: result}
else:
result = result[0] if result else None
result = {name: result}
# All items consumed
if not any(filter(None, item_kwargs)):
@ -144,15 +198,13 @@ class OrderIndicator(Indicator, list):
return result
else:
assert not self.accepts_multiple
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):
@ -167,6 +219,8 @@ class OrderIndicator(Indicator, list):
else:
values = value
self.validate(values, render_path)
for value in max_occurs_iter(self.max_occurs, values):
for name, element in self.elements_nested:
if name:
@ -186,22 +240,20 @@ class OrderIndicator(Indicator, list):
if element_value is not None or not element.is_optional:
element.render(parent, element_value, child_path)
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,)
def validate(self, value, render_path):
for item in value:
if item is NotSet:
raise ValidationError("No value set", path=render_path)
def signature(self, schema=None, standalone=True):
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)))
if isinstance(element, Indicator):
parts.append(element.signature(schema, standalone=False))
else:
parts.append('%s: %s' % (name, element.signature(depth)))
value = element.signature(schema, standalone=False)
parts.append('%s: %s' % (name, value))
part = ', '.join(parts)
if self.accepts_multiple:
@ -215,7 +267,25 @@ class All(OrderIndicator):
"""
def __init__(self, elements=None, min_occurs=1, max_occurs=1,
consume_other=False):
super(All, self).__init__(elements, min_occurs, max_occurs)
self._consume_other = consume_other
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
"""Consume matching xmlelements
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:rtype: dict or None
"""
result = OrderedDict()
expected_tags = {element.qname for __, element in self.elements}
consumed_tags = set()
@ -236,10 +306,18 @@ class All(OrderIndicator):
result[name] = element.parse_xmlelements(
sub_elements, schema, context=context)
if self._consume_other and xmlelements:
result['_raw_elements'] = list(xmlelements)
xmlelements.clear()
return result
class Choice(OrderIndicator):
"""Permits one and only one of the elements contained in the group."""
def parse_args(self, args, index=0):
if args:
raise TypeError("Choice elements only work with keyword arguments")
@property
def is_optional(self):
@ -250,49 +328,59 @@ class Choice(OrderIndicator):
return OrderedDict()
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
"""Return a dictionary"""
"""Consume matching xmlelements
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:rtype: dict or None
"""
result = []
for _unused in max_occurs_iter(self.max_occurs):
if not xmlelements:
break
for node in list(xmlelements):
# Choose out of multiple
options = []
for element_name, element in self.elements_nested:
# Choose out of multiple
options = []
for element_name, element in self.elements_nested:
local_xmlelements = copy.copy(xmlelements)
local_xmlelements = copy.copy(xmlelements)
try:
sub_result = element.parse_xmlelements(
xmlelements=local_xmlelements,
schema=schema,
name=element_name,
context=context)
except UnexpectedElementError:
continue
try:
sub_result = element.parse_xmlelements(
local_xmlelements, schema, context=context)
except UnexpectedElementError:
continue
if isinstance(element, Element):
sub_result = {element_name: sub_result}
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))
num_consumed = len(xmlelements) - len(local_xmlelements)
if num_consumed:
options.append((num_consumed, sub_result))
if not options:
xmlelements = []
break
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
# 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}
@ -308,7 +396,7 @@ class Choice(OrderIndicator):
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).
2. Passing the choice elements into the `name` kwarg (_value_1) (nested).
This case is required when multiple choice elements are given.
:param name: Name of the choice element (_value_1)
@ -320,6 +408,8 @@ class Choice(OrderIndicator):
"""
if name and name in available_kwargs:
assert self.accepts_multiple
values = kwargs[name] or []
available_kwargs.remove(name)
result = []
@ -327,9 +417,9 @@ class Choice(OrderIndicator):
if isinstance(values, dict):
values = [values]
# TODO: Use most greedy choice instead of first matching
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):
@ -356,9 +446,9 @@ class Choice(OrderIndicator):
# When choice elements are specified directly in the kwargs
found = False
for i, choice in enumerate(self):
for name, choice in self.elements_nested:
temp_kwargs = copy.copy(available_kwargs)
subresult = choice.parse_kwargs(kwargs, None, temp_kwargs)
subresult = choice.parse_kwargs(kwargs, name, temp_kwargs)
if subresult:
if not any(subresult.values()):
@ -388,12 +478,24 @@ class Choice(OrderIndicator):
if not self.accepts_multiple:
value = [value]
self.validate(value, render_path)
for item in value:
result = self._find_element_to_render(item)
if result:
element, choice_value = result
element.render(parent, choice_value, render_path)
def validate(self, value, render_path):
found = 0
for item in value:
result = self._find_element_to_render(item)
if result:
found += 1
if not found and not self.is_optional:
raise ValidationError("Missing choice values", path=render_path)
def accept(self, values):
"""Return the number of values which are accepted by this choice.
@ -403,15 +505,24 @@ class Choice(OrderIndicator):
nums = set()
for name, element in self.elements_nested:
if isinstance(element, Element):
if name in values and values[name]:
nums.add(1)
if self.accepts_multiple:
if all(name in item and item[name] for item in values):
nums.add(1)
else:
if name in values and values[name]:
nums.add(1)
else:
num = element.accept(values)
nums.add(num)
return max(nums) if nums else 0
def _find_element_to_render(self, value):
"""Return a tuple (element, value) for the best matching choice"""
"""Return a tuple (element, value) for the best matching choice.
This is used to decide which choice child is best suitable for
rendering the available data.
"""
matches = []
for name, element in self.elements_nested:
@ -441,13 +552,13 @@ class Choice(OrderIndicator):
matches = sorted(matches, key=operator.itemgetter(0), reverse=True)
return matches[0][1:]
def signature(self, depth=()):
def signature(self, schema=None, standalone=True):
parts = []
for name, element in self.elements_nested:
if isinstance(element, OrderIndicator):
parts.append('{%s}' % (element.signature(depth)))
parts.append('{%s}' % (element.signature(schema, standalone=False)))
else:
parts.append('{%s: %s}' % (name, element.signature(depth)))
parts.append('{%s: %s}' % (name, element.signature(schema, standalone=False)))
part = '(%s)' % ' | '.join(parts)
if self.accepts_multiple:
return '%s[]' % (part,)
@ -455,17 +566,42 @@ class Choice(OrderIndicator):
class Sequence(OrderIndicator):
"""Requires the elements in the group to appear in the specified sequence
within the containing element.
"""
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
"""Consume matching xmlelements
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:rtype: dict or None
"""
result = []
if self.accepts_multiple:
assert name
for _unused in max_occurs_iter(self.max_occurs):
if not xmlelements:
break
item_result = OrderedDict()
for elm_name, element in self.elements:
item_subresult = element.parse_xmlelements(
xmlelements, schema, name, context=context)
try:
item_subresult = element.parse_xmlelements(
xmlelements, schema, name, context=context)
except UnexpectedElementError:
if schema.strict:
raise
item_subresult = None
# Unwrap if allowed
if isinstance(element, OrderIndicator):
@ -480,7 +616,6 @@ class Sequence(OrderIndicator):
if not self.accepts_multiple:
return result[0] if result else None
return {name: result}
@ -498,15 +633,8 @@ class Group(Indicator):
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())
return self.signature()
def __iter__(self, *args, **kwargs):
for item in self.child:
@ -518,13 +646,28 @@ class Group(Indicator):
return [('_value_1', self.child)]
return self.child.elements
def clone(self, name, min_occurs=1, max_occurs=1):
return self.__class__(
name=name,
child=self.child,
min_occurs=min_occurs,
max_occurs=max_occurs)
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.
"""
return self.child.accept(values)
def parse_args(self, args, index=0):
return self.child.parse_args(args, index)
def parse_kwargs(self, kwargs, name, available_kwargs):
if self.accepts_multiple:
if name not in kwargs:
return {}, kwargs
return {}
available_kwargs.remove(name)
item_kwargs = kwargs[name]
@ -536,6 +679,11 @@ class Group(Indicator):
subresult = self.child.parse_kwargs(
sub_kwargs, sub_name, available_sub_kwargs)
if available_sub_kwargs:
raise TypeError((
"%s() got an unexpected keyword argument %r."
) % (self, list(available_sub_kwargs)[0]))
if subresult:
result.append(subresult)
if result:
@ -545,6 +693,19 @@ class Group(Indicator):
return result
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
"""Consume matching xmlelements
:param xmlelements: Dequeue of XML element objects
:type xmlelements: collections.deque of lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param name: The name of the parent element
:type name: str
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:rtype: dict or None
"""
result = []
for _unused in max_occurs_iter(self.max_occurs):
@ -552,16 +713,29 @@ class Group(Indicator):
self.child.parse_xmlelements(
xmlelements, schema, name, context=context)
)
if not xmlelements:
break
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 render(self, parent, value, render_path):
if not isinstance(value, list):
values = [value]
else:
values = value
for value in values:
self.child.render(parent, value, render_path)
def resolve(self):
self.child = self.child.resolve()
return self
def signature(self, depth=()):
return self.child.signature(depth)
def signature(self, schema=None, standalone=True):
name = create_prefixed_name(self.qname, schema)
if standalone:
return '%s(%s)' % (
name, self.child.signature(schema, standalone=False))
else:
return self.child.signature(schema, standalone=False)

View File

@ -1,8 +1,15 @@
"""
zeep.xsd.elements.references
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ref* objecs are only used temporarily between parsing the schema and resolving
all the elements.
"""
__all__ = ['RefElement', 'RefAttribute', 'RefAttributeGroup', 'RefGroup']
class RefElement(object):
def __init__(self, tag, ref, schema, is_qualified=False,
min_occurs=1, max_occurs=1):
self._ref = ref
@ -37,4 +44,7 @@ class RefAttributeGroup(RefElement):
class RefGroup(RefElement):
def resolve(self):
return self._schema.get_group(self._ref)
elm = self._schema.get_group(self._ref)
elm = elm.clone(
elm.qname, min_occurs=self.min_occurs, max_occurs=self.max_occurs)
return elm

View File

@ -3,8 +3,7 @@ from collections import OrderedDict
from lxml import etree
from zeep import exceptions
from zeep.xsd import const
from zeep import exceptions, ns
from zeep.xsd.elements import builtins as xsd_builtins_elements
from zeep.xsd.types import builtins as xsd_builtins_types
from zeep.xsd.visitor import SchemaVisitor
@ -15,13 +14,24 @@ logger = logging.getLogger(__name__)
class Schema(object):
"""A schema is a collection of schema documents."""
def __init__(self, node=None, transport=None, location=None):
def __init__(self, node=None, transport=None, location=None, strict=True):
"""
:param node:
:param transport:
:param location:
:param strict: Boolean to indicate if the parsing is strict (default)
"""
self.strict = strict
self._transport = transport
self._documents = OrderedDict()
self._prefix_map_auto = {}
self._prefix_map_custom = {}
self._load_default_documents()
if not isinstance(node, list):
nodes = [node] if node is not None else []
else:
@ -44,6 +54,12 @@ class Schema(object):
})
return retval
@property
def root_document(self):
return next(
(doc for doc in self.documents if not doc._is_internal),
None)
@property
def is_empty(self):
"""Boolean to indicate if this schema contains any types or elements"""
@ -55,25 +71,38 @@ class Schema(object):
@property
def elements(self):
"""Yield all globla xsd.Type objects"""
"""Yield all globla xsd.Type objects
:rtype: Iterable of zeep.xsd.Element
"""
seen = set()
for document in self.documents:
for element in document._elements.values():
yield element
if element.qname not in seen:
yield element
seen.add(element.qname)
@property
def types(self):
"""Yield all globla xsd.Type objects"""
"""Yield all global xsd.Type objects
:rtype: Iterable of zeep.xsd.ComplexType
"""
seen = set()
for document in self.documents:
for type_ in document._types.values():
yield type_
if type_.qname not in seen:
yield type_
seen.add(type_.qname)
def __repr__(self):
if self._documents:
main_doc = next(self.documents)
location = main_doc._location
else:
location = '<none>'
return '<Schema(location=%r)>' % location
main_doc = self.root_document
if main_doc:
return '<Schema(location=%r, tns=%r)>' % (
main_doc._location, main_doc._target_namespace)
return '<Schema()>'
def add_documents(self, schema_nodes, location):
documents = []
@ -87,49 +116,51 @@ class Schema(object):
self._prefix_map_auto = self._create_prefix_map()
def get_element(self, qname):
"""Return a global xsd.Element object with the given qname"""
"""Return a global xsd.Element object with the given qname
:rtype: zeep.xsd.Group
"""
qname = self._create_qname(qname)
if qname.text in xsd_builtins_elements.default_elements:
return xsd_builtins_elements.default_elements[qname]
# Handle XSD namespace items
if qname.namespace == const.NS_XSD:
try:
return xsd_builtins_elements.default_elements[qname]
except KeyError:
raise exceptions.LookupError("No such type %r" % qname.text)
return self._get_instance(qname, 'get_element', 'element')
def get_type(self, qname, fail_silently=False):
"""Return a global xsd.Type object with the given qname"""
"""Return a global xsd.Type object with the given qname
:rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType
"""
qname = self._create_qname(qname)
# Handle XSD namespace items
if qname.namespace == const.NS_XSD:
try:
return xsd_builtins_types.default_types[qname]
except KeyError:
raise exceptions.LookupError("No such type %r" % qname.text)
try:
return self._get_instance(qname, 'get_type', 'type')
except exceptions.NamespaceError as exc:
if fail_silently:
logger.info(str(exc))
logger.debug(str(exc))
else:
raise
def get_group(self, qname):
"""Return a global xsd.Group object with the given qname"""
"""Return a global xsd.Group object with the given qname.
:rtype: zeep.xsd.Group
"""
return self._get_instance(qname, 'get_group', 'group')
def get_attribute(self, qname):
"""Return a global xsd.attributeGroup object with the given qname"""
"""Return a global xsd.attributeGroup object with the given qname
:rtype: zeep.xsd.Attribute
"""
return self._get_instance(qname, 'get_attribute', 'attribute')
def get_attribute_group(self, qname):
"""Return a global xsd.attributeGroup object with the given qname"""
"""Return a global xsd.attributeGroup object with the given qname
:rtype: zeep.xsd.AttributeGroup
"""
return self._get_instance(qname, 'get_attribute_group', 'attributeGroup')
def set_ns_prefix(self, prefix, namespace):
@ -144,6 +175,18 @@ class Schema(object):
except KeyError:
raise ValueError("No such prefix %r" % prefix)
def get_shorthand_for_ns(self, namespace):
for prefix, other_namespace in self._prefix_map_auto.items():
if namespace == other_namespace:
return prefix
for prefix, other_namespace in self._prefix_map_custom.items():
if namespace == other_namespace:
return prefix
if namespace == 'http://schemas.xmlsoap.org/soap/envelope/':
return 'soap-env'
return namespace
def create_new_document(self, node, url, base_url=None):
namespace = node.get('targetNamespace') if node is not None else None
if base_url is None:
@ -160,6 +203,21 @@ class Schema(object):
self._add_schema_document(document)
self._prefix_map_auto = self._create_prefix_map()
def _load_default_documents(self):
schema = SchemaDocument(ns.XSD, None, None)
for cls in xsd_builtins_types._types:
instance = cls(is_global=True)
schema.register_type(cls._default_qname, instance)
for cls in xsd_builtins_elements._elements:
instance = cls()
schema.register_element(cls.qname, instance)
schema._is_internal = True
self._add_schema_document(schema)
return schema
def _get_instance(self, qname, method_name, name):
"""Return an object from one of the SchemaDocument's"""
qname = self._create_qname(qname)
@ -185,6 +243,8 @@ class Schema(object):
This also expands the shorthand notation.
:rtype: lxml.etree.QNaame
"""
if isinstance(name, etree.QName):
return name
@ -205,26 +265,45 @@ class Schema(object):
prefix_map = {
'xsd': 'http://www.w3.org/2001/XMLSchema',
}
for i, namespace in enumerate(self._documents.keys()):
if namespace is None:
i = 0
for namespace in self._documents.keys():
if namespace is None or namespace in prefix_map.values():
continue
prefix_map['ns%d' % i] = namespace
i += 1
return prefix_map
def _has_schema_document(self, namespace):
"""Return a boolean if there is a SchemaDocumnet for the namespace.
:rtype: boolean
"""
return namespace in self._documents
def _add_schema_document(self, document):
logger.info("Add document with tns %s to schema %s", document.namespace, id(self))
logger.debug("Add document with tns %s to schema %s", document.namespace, id(self))
documents = self._documents.setdefault(document.namespace, [])
documents.append(document)
def _get_schema_document(self, namespace, location):
"""Return a list of SchemaDocument's for the given namespace AND
location.
:rtype: SchemaDocument
"""
for document in self._documents.get(namespace, []):
if document._location == location:
return document
def _get_schema_documents(self, namespace, fail_silently=False):
"""Return a list of SchemaDocument's for the given namespace.
:rtype: list of SchemaDocument
"""
if namespace not in self._documents:
if fail_silently:
return []
@ -241,7 +320,7 @@ class SchemaDocument(object):
self._base_url = base_url or location
self._location = location
self._target_namespace = namespace
self._elm_instances = []
self._is_internal = False
self._attribute_groups = {}
self._attributes = {}
@ -283,7 +362,7 @@ class SchemaDocument(object):
visitor.visit_schema(node)
def resolve(self):
logger.info("Resolving in schema %s", self)
logger.debug("Resolving in schema %s", self)
if self._resolved:
return
@ -294,10 +373,23 @@ class SchemaDocument(object):
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
try:
for key, obj in val.items():
new = obj.resolve()
assert new is not None, "resolve() should return an object"
val[key] = new
except exceptions.LookupError as exc:
raise exceptions.LookupError(
(
"Unable to resolve %(item_name)s %(qname)s in "
"%(file)s. (via %(parent)s)"
) % {
'item_name': exc.item_name,
'item_name': exc.item_name,
'qname': exc.qname,
'file': exc.location,
'parent': obj.qname,
})
_resolve_dict(self._attribute_groups)
_resolve_dict(self._attributes)
@ -305,10 +397,6 @@ class SchemaDocument(object):
_resolve_dict(self._groups)
_resolve_dict(self._types)
for element in self._elm_instances:
element.resolve()
self._elm_instances = []
def register_import(self, namespace, schema):
schemas = self._imports.setdefault(namespace, [])
schemas.append(schema)
@ -350,23 +438,43 @@ class SchemaDocument(object):
self._attribute_groups[name] = value
def get_type(self, qname):
"""Return a xsd.Type object from this schema"""
"""Return a xsd.Type object from this schema
:rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType
"""
return self._get_instance(qname, self._types, 'type')
def get_element(self, qname):
"""Return a xsd.Element object from this schema"""
"""Return a xsd.Element object from this schema
:rtype: zeep.xsd.Element
"""
return self._get_instance(qname, self._elements, 'element')
def get_group(self, qname):
"""Return a xsd.Group object from this schema"""
"""Return a xsd.Group object from this schema.
:rtype: zeep.xsd.Group
"""
return self._get_instance(qname, self._groups, 'group')
def get_attribute(self, qname):
"""Return a xsd.Attribute object from this schema"""
"""Return a xsd.Attribute object from this schema
:rtype: zeep.xsd.Attribute
"""
return self._get_instance(qname, self._attributes, 'attribute')
def get_attribute_group(self, qname):
"""Return a xsd.AttributeGroup object from this schema"""
"""Return a xsd.AttributeGroup object from this schema
:rtype: zeep.xsd.AttributeGroup
"""
return self._get_instance(qname, self._attribute_groups, 'attributeGroup')
def _get_instance(self, qname, items, item_name):
@ -377,10 +485,13 @@ class SchemaDocument(object):
raise exceptions.LookupError((
"No %(item_name)s '%(localname)s' in namespace %(namespace)s. " +
"Available %(item_name_plural)s are: %(known_items)s"
) % {
'item_name': item_name,
'item_name_plural': item_name + 's',
'localname': qname.localname,
'namespace': qname.namespace,
'known_items': known_items or ' - '
})
) % {
'item_name': item_name,
'item_name_plural': item_name + 's',
'localname': qname.localname,
'namespace': qname.namespace,
'known_items': known_items or ' - '
},
qname=qname,
item_name=item_name,
location=self._location)

View File

@ -12,6 +12,11 @@ __all__ = ['AnyType']
class AnyType(Type):
_default_qname = xsd_ns('anyType')
_attributes_unwrapped = []
_element = None
def __call__(self, value=None):
return value or ''
def render(self, parent, value, xsd_type=None, render_path=None):
if isinstance(value, AnyObject):
@ -27,7 +32,22 @@ class AnyType(Type):
parent.text = self.xmlvalue(value)
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
context=None):
context=None, schema_type=None):
"""Consume matching xmlelements and call parse() on each
:param xmlelement: XML element objects
:type xmlelement: lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param allow_none: Allow none
:type allow_none: bool
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:param schema_type: The original type (not overriden via xsi:type)
:type schema_type: zeep.xsd.types.base.Type
:rtype: dict or None
"""
xsi_type = qname_attr(xmlelement, xsi_ns('type'))
xsi_nil = xmlelement.get(xsi_ns('nil'))
children = list(xmlelement.getchildren())
@ -42,8 +62,14 @@ class AnyType(Type):
xsd_type = schema.get_type(xsi_type, fail_silently=True)
# If we were unable to resolve a type for the xsi:type (due to
# buggy soap servers) then we just return the lxml element.
# buggy soap servers) then we just return the text or lxml element.
if not xsd_type:
logger.debug(
"Unable to resolve type for %r, returning raw data",
xsi_type.text)
if xmlelement.text:
return xmlelement.text
return children
# If the xsd_type is xsd:anyType then we will recurs so ignore
@ -72,3 +98,6 @@ class AnyType(Type):
def pythonvalue(self, value, schema=None):
return value
def signature(self, schema=None, standalone=True):
return 'xsd:anyType'

View File

@ -1,3 +1,8 @@
from zeep.xsd.utils import create_prefixed_name
__all__ = ['Type']
class Type(object):
def __init__(self, qname=None, is_global=False):
@ -6,9 +11,16 @@ class Type(object):
self._resolved = False
self.is_global = is_global
def get_prefixed_name(self, schema):
return create_prefixed_name(self.qname, schema)
def accept(self, value):
raise NotImplementedError
@property
def accepted_types(self):
return tuple()
def validate(self, value, required=False):
return
@ -23,7 +35,7 @@ class Type(object):
return {}
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
context=None):
context=None, schema_type=None):
raise NotImplementedError(
'%s.parse_xmlelement() is not implemented' % self.__class__.__name__)
@ -51,61 +63,5 @@ class Type(object):
return []
@classmethod
def signature(cls, depth=()):
def signature(cls, schema=None, standalone=True):
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, xsd_type=None, render_path=None):
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',
}
from zeep.xsd.types.collection import UnionType # FIXME
from zeep.xsd.types.simple import AnySimpleType # FIXME
if issubclass(base.__class__, UnionType):
xsd_type = type(self.name, (base.__class__,), cls_attributes)
return xsd_type(base.item_types)
elif issubclass(base.__class__, AnySimpleType):
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)

View File

@ -17,6 +17,11 @@ class ParseError(ValueError):
pass
class BuiltinType(object):
def __init__(self, qname=None, is_global=False):
super(BuiltinType, self).__init__(qname, is_global=True)
def check_no_collection(func):
def _wrapper(self, value):
if isinstance(value, (list, dict, set)):
@ -30,7 +35,7 @@ def check_no_collection(func):
##
# Primitive types
class String(AnySimpleType):
class String(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('string')
accepted_types = six.string_types
@ -44,13 +49,13 @@ class String(AnySimpleType):
return value
class Boolean(AnySimpleType):
class Boolean(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('boolean')
accepted_types = (bool,)
@check_no_collection
def xmlvalue(self, value):
return 'true' if value else 'false'
return 'true' if value and value not in ('false', '0') else 'false'
def pythonvalue(self, value):
"""Return True if the 'true' or '1'. 'false' and '0' are legal false
@ -60,7 +65,7 @@ class Boolean(AnySimpleType):
return value in ('true', '1')
class Decimal(AnySimpleType):
class Decimal(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('decimal')
accepted_types = (_Decimal, float) + six.string_types
@ -72,7 +77,7 @@ class Decimal(AnySimpleType):
return _Decimal(value)
class Float(AnySimpleType):
class Float(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('float')
accepted_types = (float, _Decimal) + six.string_types
@ -83,7 +88,7 @@ class Float(AnySimpleType):
return float(value)
class Double(AnySimpleType):
class Double(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('double')
accepted_types = (_Decimal, float) + six.string_types
@ -95,7 +100,7 @@ class Double(AnySimpleType):
return float(value)
class Duration(AnySimpleType):
class Duration(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('duration')
accepted_types = (isodate.duration.Duration,) + six.string_types
@ -107,7 +112,7 @@ class Duration(AnySimpleType):
return isodate.parse_duration(value)
class DateTime(AnySimpleType):
class DateTime(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('dateTime')
accepted_types = (datetime.datetime,) + six.string_types
@ -131,7 +136,7 @@ class DateTime(AnySimpleType):
return isodate.parse_datetime(value)
class Time(AnySimpleType):
class Time(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('time')
accepted_types = (datetime.time,) + six.string_types
@ -145,7 +150,7 @@ class Time(AnySimpleType):
return isodate.parse_time(value)
class Date(AnySimpleType):
class Date(BuiltinType, AnySimpleType):
_default_qname = xsd_ns('date')
accepted_types = (datetime.date,) + six.string_types
@ -159,7 +164,7 @@ class Date(AnySimpleType):
return isodate.parse_date(value)
class gYearMonth(AnySimpleType):
class gYearMonth(BuiltinType, AnySimpleType):
"""gYearMonth represents a specific gregorian month in a specific gregorian
year.
@ -186,7 +191,7 @@ class gYearMonth(AnySimpleType):
_parse_timezone(group['timezone']))
class gYear(AnySimpleType):
class gYear(BuiltinType, AnySimpleType):
"""gYear represents a gregorian calendar year.
Lexical representation: CCYY
@ -209,7 +214,7 @@ class gYear(AnySimpleType):
return (int(group['year']), _parse_timezone(group['timezone']))
class gMonthDay(AnySimpleType):
class gMonthDay(BuiltinType, AnySimpleType):
"""gMonthDay is a gregorian date that recurs, specifically a day of the
year such as the third of May.
@ -237,7 +242,7 @@ class gMonthDay(AnySimpleType):
_parse_timezone(group['timezone']))
class gDay(AnySimpleType):
class gDay(BuiltinType, AnySimpleType):
"""gDay is a gregorian day that recurs, specifically a day of the month
such as the 5th of the month
@ -261,7 +266,7 @@ class gDay(AnySimpleType):
return (int(group['day']), _parse_timezone(group['timezone']))
class gMonth(AnySimpleType):
class gMonth(BuiltinType, AnySimpleType):
"""gMonth is a gregorian month that recurs every year.
Lexical representation: --MM
@ -284,7 +289,7 @@ class gMonth(AnySimpleType):
return (int(group['month']), _parse_timezone(group['timezone']))
class HexBinary(AnySimpleType):
class HexBinary(BuiltinType, AnySimpleType):
accepted_types = six.string_types
_default_qname = xsd_ns('hexBinary')
@ -296,7 +301,7 @@ class HexBinary(AnySimpleType):
return value
class Base64Binary(AnySimpleType):
class Base64Binary(BuiltinType, AnySimpleType):
accepted_types = six.string_types
_default_qname = xsd_ns('base64Binary')
@ -308,7 +313,7 @@ class Base64Binary(AnySimpleType):
return base64.b64decode(value)
class AnyURI(AnySimpleType):
class AnyURI(BuiltinType, AnySimpleType):
accepted_types = six.string_types
_default_qname = xsd_ns('anyURI')
@ -320,7 +325,7 @@ class AnyURI(AnySimpleType):
return value
class QName(AnySimpleType):
class QName(BuiltinType, AnySimpleType):
accepted_types = six.string_types
_default_qname = xsd_ns('QName')
@ -332,7 +337,7 @@ class QName(AnySimpleType):
return value
class Notation(AnySimpleType):
class Notation(BuiltinType, AnySimpleType):
accepted_types = six.string_types
_default_qname = xsd_ns('NOTATION')
@ -390,6 +395,7 @@ class Entities(Entity):
class Integer(Decimal):
_default_qname = xsd_ns('integer')
accepted_types = (int, float) + six.string_types
def xmlvalue(self, value):
return str(value)
@ -484,58 +490,60 @@ def _unparse_timezone(tzinfo):
return '-%02d:%02d' % (abs(hours), minutes)
_types = [
# 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,
]
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,
]
cls._default_qname: cls(is_global=True) for cls in _types
}

View File

@ -32,8 +32,8 @@ class ListType(AnySimpleType):
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) + '[]'
def signature(self, schema=None, standalone=True):
return self.item_type.signature(schema) + '[]'
class UnionType(AnySimpleType):
@ -52,11 +52,11 @@ class UnionType(AnySimpleType):
self.item_class = base_class
return self
def signature(self, depth=()):
def signature(self, schema=None, standalone=True):
return ''
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
context=None):
context=None, schema_type=None):
if self.item_class:
return self.item_class().parse_xmlelement(
xmlelement, schema, allow_none, context)

View File

@ -6,14 +6,14 @@ from itertools import chain
from cached_property import threaded_cached_property
from zeep.exceptions import UnexpectedElementError, XMLParseError
from zeep.xsd.const import xsi_ns, SkipValue, NotSet
from zeep.xsd.const import NotSet, SkipValue, xsi_ns
from zeep.xsd.elements import (
Any, AnyAttribute, AttributeGroup, Choice, Element, Group, Sequence)
from zeep.xsd.elements.indicators import OrderIndicator
from zeep.xsd.types.any import AnyType
from zeep.xsd.types.simple import AnySimpleType
from zeep.xsd.utils import NamePrefixGenerator
from zeep.xsd.valueobjects import CompoundValue
from zeep.xsd.valueobjects import CompoundValue, ArrayValue
logger = logging.getLogger(__name__)
@ -33,14 +33,24 @@ class ComplexType(AnyType):
self._attributes = attributes or []
self._restriction = restriction
self._extension = extension
self._extension_types = tuple()
super(ComplexType, self).__init__(qname=qname, is_global=is_global)
def __call__(self, *args, **kwargs):
if self._array_type:
return self._array_class(*args, **kwargs)
return self._value_class(*args, **kwargs)
@property
def accepted_types(self):
return (self._value_class,)
return (self._value_class,) + self._extension_types
@threaded_cached_property
def _array_class(self):
assert self._array_type
return type(
self.__class__.__name__, (ArrayValue,),
{'_xsd_type': self, '__module__': 'zeep.objects'})
@threaded_cached_property
def _value_class(self):
@ -94,25 +104,43 @@ class ComplexType(AnyType):
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:
if self._array_type:
name = generator.get_name()
if isinstance(self._element, Group):
return [(name, Sequence([
Any(max_occurs='unbounded', restrict=array_type.array_type)
result = [(name, Sequence([
Any(max_occurs='unbounded', restrict=self._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))
result = [(name, self._element)]
else:
# _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"""
@property
def _array_type(self):
attrs = {attr.qname.text: attr for attr in self._attributes if attr.qname}
array_type = attrs.get('{http://schemas.xmlsoap.org/soap/encoding/}arrayType')
return array_type
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
context=None, schema_type=None):
"""Consume matching xmlelements and call parse() on each
:param xmlelement: XML element objects
:type xmlelement: lxml.etree._Element
:param schema: The parent XML schema
:type schema: zeep.xsd.Schema
:param allow_none: Allow none
:type allow_none: bool
:param context: Optional parsing context (for inline schemas)
:type context: zeep.xsd.context.XmlParserContext
:param schema_type: The original type (not overriden via xsi:type)
:type schema_type: zeep.xsd.types.base.Type
:rtype: dict or None
"""
# If this is an empty complexType (<xsd:complexType name="x"/>)
if not self.attributes and not self.elements:
return None
@ -134,6 +162,7 @@ class ComplexType(AnyType):
# Parse elements. These are always indicator elements (all, choice,
# group, sequence)
assert len(self.elements_nested) < 2
for name, element in self.elements_nested:
try:
result = element.parse_xmlelements(
@ -145,7 +174,10 @@ class ComplexType(AnyType):
# Check if all children are consumed (parsed)
if elements:
raise XMLParseError("Unexpected element %r" % elements[0].tag)
if schema.strict:
raise XMLParseError("Unexpected element %r" % elements[0].tag)
else:
init_kwargs['_raw_elements'] = elements
# Parse attributes
if attributes:
@ -158,12 +190,21 @@ class ComplexType(AnyType):
else:
init_kwargs[name] = attribute.parse(attributes)
return self(**init_kwargs)
value = self._value_class(**init_kwargs)
schema_type = schema_type or self
if schema_type and getattr(schema_type, '_array_type', None):
value = schema_type._array_class.from_value_object(value)
return value
def render(self, parent, value, xsd_type=None, render_path=None):
"""Serialize the given value lxml.Element subelements on the parent
element.
:type parent: lxml.etree._Element
:type value: Union[list, dict, zeep.xsd.valueobjects.CompoundValue]
:type xsd_type: zeep.xsd.types.base.Type
:param render_path: list
"""
if not render_path:
render_path = [self.name]
@ -171,12 +212,24 @@ class ComplexType(AnyType):
if not self.elements_nested and not self.attributes:
return
if isinstance(value, ArrayValue):
value = value.as_value_object()
# Render attributes
for name, attribute in self.attributes:
attr_value = value[name] if name in value else NotSet
child_path = render_path + [name]
attribute.render(parent, attr_value, child_path)
if (
len(self.elements_nested) == 1
and isinstance(value, self.accepted_types)
and not isinstance(value, (list, dict, CompoundValue))
):
element = self.elements_nested[0][1]
element.type.render(parent, value, None, child_path)
return
# Render sub elements
for name, element in self.elements_nested:
if isinstance(element, Element) or element.accepts_multiple:
@ -186,6 +239,7 @@ class ComplexType(AnyType):
element_value = value
child_path = list(render_path)
# We want to explicitly skip this sub-element
if element_value is SkipValue:
continue
@ -201,6 +255,19 @@ class ComplexType(AnyType):
parent.set(xsi_ns('type'), xsd_type.qname)
def parse_kwargs(self, kwargs, name, available_kwargs):
"""Parse the kwargs for this type and return the accepted data as
a dict.
:param kwargs: The kwargs
:type kwargs: dict
:param name: The name as which this type is registered in the parent
:type name: str
:param available_kwargs: The kwargs keys which are still available,
modified in place
:type available_kwargs: set
:rtype: dict
"""
value = None
name = name or self.name
@ -213,28 +280,27 @@ class ComplexType(AnyType):
return {}
def _create_object(self, value, name):
"""Return the value as a CompoundValue object"""
"""Return the value as a CompoundValue object
:type value: str
:type value: list, dict, CompoundValue
"""
if value is None:
return None
if isinstance(value, list):
if isinstance(value, list) and not self._array_type:
return [self._create_object(val, name) for val in value]
if isinstance(value, CompoundValue):
if isinstance(value, CompoundValue) or value is SkipValue:
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)))
# Try to automatically create an object. This might fail if there
# are multiple required arguments.
return self(value)
def resolve(self):
"""Resolve all sub elements and types"""
@ -242,9 +308,6 @@ class ComplexType(AnyType):
return self._resolved
self._resolved = self
if self._element:
self._element = self._element.resolve()
resolved = []
for attribute in self._attributes:
value = attribute.resolve()
@ -258,18 +321,17 @@ class ComplexType(AnyType):
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
if self._element:
self._element = self._element.resolve()
return self._resolved
def extend(self, base):
"""Create a new complextype instance which is the current type
"""Create a new ComplexType instance which is the current type
extending the given base type.
Used for handling xsd:extension tags
@ -277,6 +339,9 @@ class ComplexType(AnyType):
TODO: Needs a rewrite where the child containers are responsible for
the extend functionality.
:type base: zeep.xsd.types.base.Type
:rtype base: zeep.xsd.types.base.Type
"""
if isinstance(base, ComplexType):
base_attributes = base._attributes_unwrapped
@ -301,6 +366,9 @@ class ComplexType(AnyType):
# container a placeholder element).
element = []
if self._element and base_element:
self._element = self._element.resolve()
base_element = base_element.resolve()
element = self._element.clone(self._element.name)
if isinstance(base_element, OrderIndicator):
if isinstance(self._element, Choice):
@ -323,7 +391,10 @@ class ComplexType(AnyType):
new = self.__class__(
element=element,
attributes=attributes,
qname=self.qname)
qname=self.qname,
is_global=self.is_global)
new._extension_types = base.accepted_types
return new
def restrict(self, base):
@ -332,6 +403,10 @@ class ComplexType(AnyType):
Used for handling xsd:restriction
:type base: zeep.xsd.types.base.Type
:rtype base: zeep.xsd.types.base.Type
"""
attributes = list(
chain(base._attributes_unwrapped, self._attributes_unwrapped))
@ -344,33 +419,30 @@ class ComplexType(AnyType):
new_attributes['##any'] = attr
else:
new_attributes[attr.qname.text] = attr
attributes = new_attributes.values()
attributes = list(new_attributes.values())
if base._element:
base._element.resolve()
new = self.__class__(
element=self._element or base._element,
attributes=attributes,
qname=self.qname)
qname=self.qname,
is_global=self.is_global)
return new.resolve()
def signature(self, depth=()):
if len(depth) > 0 and self.is_global:
return self.name
def signature(self, schema=None, standalone=True):
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)
part = element.signature(schema, standalone=False)
parts.append(part)
for name, attribute in self.attributes:
part = '%s: %s' % (name, attribute.signature(depth))
part = '%s: %s' % (name, attribute.signature(schema, standalone=False))
parts.append(part)
value = ', '.join(parts)
if len(depth) > 1:
value = '{%s}' % value
return value
if standalone:
return '%s(%s)' % (self.get_prefixed_name(schema), value)
else:
return value

View File

@ -4,7 +4,7 @@ import six
from lxml import etree
from zeep.exceptions import ValidationError
from zeep.xsd.const import NS_XSD, xsd_ns
from zeep.xsd.const import xsd_ns
from zeep.xsd.types.any import AnyType
logger = logging.getLogger(__name__)
@ -54,7 +54,7 @@ class AnySimpleType(AnyType):
return '%s(value)' % (self.__class__.__name__)
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
context=None):
context=None, schema_type=None):
if xmlelement.text is None:
return
try:
@ -70,10 +70,8 @@ class AnySimpleType(AnyType):
def render(self, parent, value, xsd_type=None, render_path=None):
parent.text = self.xmlvalue(value)
def signature(self, depth=()):
if self.qname.namespace == NS_XSD:
return 'xsd:%s' % self.name
return self.name
def signature(self, schema=None, standalone=True):
return self.get_prefixed_name(schema)
def validate(self, value, required=False):
if required and value is None:

View File

@ -0,0 +1,56 @@
from zeep.xsd.types.base import Type
from zeep.xsd.types.collection import UnionType # FIXME
from zeep.xsd.types.simple import AnySimpleType # FIXME
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.text)
def render(self, parent, value, xsd_type=None, render_path=None):
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__, AnySimpleType):
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)

View File

@ -1,10 +1,6 @@
from defusedxml.lxml import fromstring
from lxml import etree
from six.moves import range
from six.moves.urllib.parse import urlparse
from zeep.exceptions import XMLSyntaxError
from zeep.parser import absolute_location
from zeep import ns
class NamePrefixGenerator(object):
@ -31,34 +27,6 @@ class UniqueNameGenerator(object):
return name
class ImportResolver(etree.Resolver):
"""Custom lxml resolve to use the transport object"""
def __init__(self, transport):
self.transport = transport
def resolve(self, url, pubid, context):
if urlparse(url).scheme in ('http', 'https'):
content = self.transport.load(url)
return self.resolve_string(content, context)
def parse_xml(content, transport, base_url=None):
parser = etree.XMLParser(remove_comments=True, resolve_entities=False)
parser.resolvers.add(ImportResolver(transport))
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, base_url=None):
if base_url:
url = absolute_location(url, base_url)
response = transport.load(url)
return parse_xml(response, transport, base_url)
def max_occurs_iter(max_occurs, items=None):
assert max_occurs is not None
generator = range(0, max_occurs if max_occurs != 'unbounded' else 2**31-1)
@ -69,3 +37,27 @@ def max_occurs_iter(max_occurs, items=None):
else:
for i in generator:
yield i
def create_prefixed_name(qname, schema):
"""Convert a QName to a xsd:name ('ns1:myType').
:type qname: lxml.etree.QName
:type schema: zeep.xsd.schema.Schema
:rtype: str
"""
if not qname:
return
if schema and qname.namespace:
prefix = schema.get_shorthand_for_ns(qname.namespace)
if prefix:
return '%s:%s' % (prefix, qname.localname)
elif qname.namespace in ns.NAMESPACE_TO_PREFIX:
prefix = ns.NAMESPACE_TO_PREFIX[qname.namespace]
return '%s:%s' % (prefix, qname.localname)
if qname.namespace:
return qname.text
return qname.localname

View File

@ -33,11 +33,46 @@ class AnyObject(object):
return self.xsd_obj
def _unpickle_compound_value(name, values):
"""Helper function to recreate pickled CompoundValue.
See CompoundValue.__reduce__
"""
cls = type(name, (CompoundValue,), {
'_xsd_type': None, '__module__': 'zeep.objects'
})
obj = cls()
obj.__values__ = values
return obj
class ArrayValue(list):
def __init__(self, items):
super(ArrayValue, self).__init__(items)
def as_value_object(self):
anon_type = type(
self.__class__.__name__, (CompoundValue,),
{'_xsd_type': self._xsd_type, '__module__': 'zeep.objects'})
return anon_type(list(self))
@classmethod
def from_value_object(cls, obj):
items = next(iter(obj.__values__.values()))
return cls(items or [])
class CompoundValue(object):
"""Represents a data object for a specific xsd:complexType."""
def __init__(self, *args, **kwargs):
values = OrderedDict()
# Can be done after unpickle
if self._xsd_type is None:
return
# Set default values
for container_name, container in self._xsd_type.elements_nested:
elm_values = container.default_value
@ -56,6 +91,9 @@ class CompoundValue(object):
values[key] = value
self.__values__ = values
def __reduce__(self):
return (_unpickle_compound_value, (self.__class__.__name__, self.__values__,))
def __contains__(self, key):
return self.__values__.__contains__(key)
@ -107,6 +145,9 @@ class CompoundValue(object):
setattr(new, attr, value)
return new
def __json__(self):
return self.__values__
def _process_signature(xsd_type, args, kwargs):
"""Return a dict with the args/kwargs mapped to the field name.
@ -169,13 +210,19 @@ def _process_signature(xsd_type, args, kwargs):
available_kwargs.remove(attribute_name)
result[attribute_name] = kwargs[attribute_name]
# _raw_elements is a special kwarg used for unexpected unparseable xml
# elements (e.g. for soap:header or when strict is disabled)
if '_raw_elements' in available_kwargs and kwargs['_raw_elements']:
result['_raw_elements'] = kwargs['_raw_elements']
available_kwargs.remove('_raw_elements')
if available_kwargs:
raise TypeError((
"%s() got an unexpected keyword argument %r. " +
"Signature: (%s)"
"Signature: `%s`"
) % (
xsd_type.qname or 'ComplexType',
next(iter(available_kwargs)),
xsd_type.signature()))
xsd_type.signature(standalone=False)))
return result

View File

@ -4,14 +4,13 @@ import re
from lxml import etree
from zeep import exceptions
from zeep.exceptions import XMLParseError
from zeep.parser import absolute_location
from zeep.loader import absolute_location, load_external
from zeep.utils import as_qname, qname_attr
from zeep.xsd import elements as xsd_elements
from zeep.xsd import types as xsd_types
from zeep.xsd.const import xsd_ns
from zeep.xsd.utils import load_external
from zeep.xsd.types.unresolved import UnresolvedCustomType, UnresolvedType
logger = logging.getLogger(__name__)
@ -37,12 +36,36 @@ class SchemaVisitor(object):
"""Visitor which processes XSD files and registers global elements and
types in the given schema.
:param schema:
:type schema: zeep.xsd.schema.Schema
:param document:
:type document: zeep.xsd.schema.SchemaDocument
"""
def __init__(self, schema, document):
self.document = document
self.schema = schema
self._includes = set()
def register_element(self, qname, instance):
self.document.register_element(qname, instance)
def register_attribute(self, name, instance):
self.document.register_attribute(name, instance)
def register_type(self, qname, instance):
self.document.register_type(qname, instance)
def register_group(self, qname, instance):
self.document.register_group(qname, instance)
def register_attribute_group(self, qname, instance):
self.document.register_attribute_group(qname, instance)
def register_import(self, namespace, document):
self.document.register_import(namespace, document)
def process(self, node, parent):
visit_func = self.visitors.get(node.tag)
if not visit_func:
@ -79,7 +102,10 @@ class SchemaVisitor(object):
return cls(node.tag, ref, self.schema, **kwargs)
def visit_schema(self, node):
"""
"""Visit the xsd:schema element and process all the child elements
Definition::
<schema
attributeFormDefault = (qualified | unqualified): unqualified
blockDefault = (#all | List of (extension | restriction | substitution) : ''
@ -97,6 +123,9 @@ class SchemaVisitor(object):
annotation*)*)
</schema>
:param node: The XML node
:type node: lxml.etree._Element
"""
assert node is not None
@ -104,12 +133,14 @@ class SchemaVisitor(object):
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)
for child in node:
self.process(child, parent=node)
def visit_import(self, node, parent):
"""
Definition::
<import
id = ID
namespace = anyURI
@ -117,6 +148,12 @@ class SchemaVisitor(object):
{any attributes with non-schema Namespace}...>
Content: (annotation?)
</import>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
schema_node = None
namespace = node.get('namespace')
@ -136,7 +173,7 @@ class SchemaVisitor(object):
document = self.schema._get_schema_document(namespace, location)
if document:
logger.debug("Returning existing schema: %r", location)
self.document.register_import(namespace, document)
self.register_import(namespace, document)
return document
# Hardcode the mapping between the xml namespace and the xsd for now.
@ -153,7 +190,10 @@ class SchemaVisitor(object):
return
# Load the XML
schema_node = load_external(location, self.schema._transport)
schema_node = load_external(
location,
self.schema._transport,
strict=self.schema.strict)
# Check if the xsd:import namespace matches the targetNamespace. If
# the xsd:import statement didn't specify a namespace then make sure
@ -168,17 +208,26 @@ class SchemaVisitor(object):
sourceline=node.sourceline)
schema = self.schema.create_new_document(schema_node, location)
self.document.register_import(namespace, schema)
self.register_import(namespace, schema)
return schema
def visit_include(self, node, parent):
"""
<include
id = ID
schemaLocation = anyURI
{any attributes with non-schema Namespace}...>
Content: (annotation?)
</include>
Definition::
<include
id = ID
schemaLocation = anyURI
{any attributes with non-schema Namespace}...>
Content: (annotation?)
</include>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
if not node.get('schemaLocation'):
raise NotImplementedError("schemaLocation is required")
@ -188,13 +237,49 @@ class SchemaVisitor(object):
return
schema_node = load_external(
location, self.schema._transport, base_url=self.document._base_url)
location, self.schema._transport,
base_url=self.document._base_url,
strict=self.schema.strict)
self._includes.add(location)
return self.visit_schema(schema_node)
# When the included document has no default namespace defined but the
# parent document does have this then we should (atleast for #360)
# transfer the default namespace to the included schema. We can't
# update the nsmap of elements in lxml so we create a new schema with
# the correct nsmap and move all the content there.
if not schema_node.nsmap.get(None) and node.nsmap.get(None):
nsmap = {None: node.nsmap[None]}
nsmap.update(schema_node.nsmap)
new = etree.Element(schema_node.tag, nsmap=nsmap)
for child in schema_node:
new.append(child)
for key, value in schema_node.attrib.items():
new.set(key, value)
schema_node = new
# Use the element/attribute form defaults from the schema while
# processing the nodes.
element_form_default = self.document._element_form
attribute_form_default = self.document._attribute_form
base_url = self.document._base_url
self.document._element_form = schema_node.get('elementFormDefault', 'unqualified')
self.document._attribute_form = schema_node.get('attributeFormDefault', 'unqualified')
self.document._base_url = absolute_location(location, self.document._base_url)
# Iterate directly over the children.
for child in schema_node:
self.process(child, parent=schema_node)
self.document._element_form = element_form_default
self.document._attribute_form = attribute_form_default
self.document._base_url = base_url
def visit_element(self, node, parent):
"""
Definition::
<element
abstract = Boolean : false
block = (#all | List of (extension | restriction | substitution))
@ -214,6 +299,12 @@ class SchemaVisitor(object):
Content: (annotation?, (
(simpleType | complexType)?, (unique | key | keyref)*))
</element>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
is_global = parent.tag == tags.schema
@ -272,16 +363,16 @@ class SchemaVisitor(object):
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)
self.register_element(qname, element)
return element
def visit_attribute(self, node, parent):
"""Declares an attribute.
Definition::
<attribute
default = string
fixed = string
@ -294,6 +385,12 @@ class SchemaVisitor(object):
{any attributes with non-schema Namespace...}>
Content: (annotation?, (simpleType?))
</attribute>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
is_global = parent.tag == tags.schema
@ -303,9 +400,8 @@ class SchemaVisitor(object):
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)
qname = as_qname(array_type, node.nsmap)
array_type = UnresolvedType(qname, self.schema)
# If the elment has a ref attribute then all other attributes cannot
# be present. Short circuit that here.
@ -316,9 +412,8 @@ class SchemaVisitor(object):
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
name = qname_attr(node, 'name', self.document._target_namespace)
else:
name = etree.QName(node.get('name'))
@ -338,15 +433,16 @@ class SchemaVisitor(object):
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)
self.register_attribute(name, attr)
return attr
def visit_simple_type(self, node, parent):
"""
Definition::
<simpleType
final = (#all | (list | union | restriction))
id = ID
@ -354,6 +450,12 @@ class SchemaVisitor(object):
{any attributes with non-schema Namespace}...>
Content: (annotation?, (restriction | list | union))
</simpleType>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
if parent.tag == tags.schema:
@ -369,8 +471,7 @@ class SchemaVisitor(object):
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)
xsd_type = UnresolvedCustomType(qname, base_type, self.schema)
elif child.tag == tags.list:
xsd_type = self.visit_list(child, node)
@ -382,23 +483,30 @@ class SchemaVisitor(object):
assert xsd_type is not None
if is_global:
self.document.register_type(qname, xsd_type)
self.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>
Definition::
<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>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
children = []
@ -448,22 +556,30 @@ class SchemaVisitor(object):
element=element, attributes=attributes, qname=qname,
is_global=is_global)
else:
xsd_type = xsd_cls(qname=qname)
xsd_type = xsd_cls(qname=qname, is_global=is_global)
if is_global:
self.document.register_type(qname, xsd_type)
self.register_type(qname, xsd_type)
return xsd_type
def visit_complex_content(self, node, parent, namespace=None):
def visit_complex_content(self, node, parent):
"""The complexContent element defines extensions or restrictions on a
complex type that contains mixed content or elements only.
Definition::
<complexContent
id = ID
mixed = Boolean
{any attributes with non-schema Namespace}...>
Content: (annotation?, (restriction | extension))
</complexContent>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
child = node.getchildren()[-1]
@ -485,16 +601,24 @@ class SchemaVisitor(object):
'extension': base,
}
def visit_simple_content(self, node, parent, namespace=None):
def visit_simple_content(self, node, parent):
"""Contains extensions or restrictions on a complexType element with
character data or a simpleType element as content and contains no
elements.
Definition::
<simpleContent
id = ID
{any attributes with non-schema Namespace}...>
Content: (annotation?, (restriction | extension))
</simpleContent>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
child = node.getchildren()[-1]
@ -505,8 +629,10 @@ class SchemaVisitor(object):
return self.visit_extension_simple_content(child, node)
raise AssertionError("Expected restriction or extension")
def visit_restriction_simple_type(self, node, parent, namespace=None):
def visit_restriction_simple_type(self, node, parent):
"""
Definition::
<restriction
base = QName
id = ID
@ -517,6 +643,12 @@ class SchemaVisitor(object):
totalDigits |fractionDigits | length | minLength |
maxLength | enumeration | whiteSpace | pattern)*))
</restriction>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
base_name = qname_attr(node, 'base')
if base_name:
@ -526,8 +658,10 @@ class SchemaVisitor(object):
if children[0].tag == tags.simpleType:
return self.visit_simple_type(children[0], node)
def visit_restriction_simple_content(self, node, parent, namespace=None):
def visit_restriction_simple_content(self, node, parent):
"""
Definition::
<restriction
base = QName
id = ID
@ -539,14 +673,22 @@ class SchemaVisitor(object):
maxLength | enumeration | whiteSpace | pattern)*
)?, ((attribute | attributeGroup)*, anyAttribute?))
</restriction>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
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):
def visit_restriction_complex_content(self, node, parent):
"""
Definition::
<restriction
base = QName
id = ID
@ -554,6 +696,12 @@ class SchemaVisitor(object):
Content: (annotation?, (group | all | choice | sequence)?,
((attribute | attributeGroup)*, anyAttribute?))
</restriction>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
base_name = qname_attr(node, 'base')
base_type = self._get_type(base_name)
@ -572,6 +720,9 @@ class SchemaVisitor(object):
def visit_extension_complex_content(self, node, parent):
"""
Definition::
<extension
base = QName
id = ID
@ -580,6 +731,12 @@ class SchemaVisitor(object):
(group | all | choice | sequence)?,
((attribute | attributeGroup)*, anyAttribute?)))
</extension>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
base_name = qname_attr(node, 'base')
base_type = self._get_type(base_name)
@ -599,6 +756,9 @@ class SchemaVisitor(object):
def visit_extension_simple_content(self, node, parent):
"""
Definition::
<extension
base = QName
id = ID
@ -616,16 +776,27 @@ class SchemaVisitor(object):
def visit_annotation(self, node, parent):
"""Defines an annotation.
Definition::
<annotation
id = ID
{any attributes with non-schema Namespace}...>
Content: (appinfo | documentation)*
</annotation>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
return
def visit_any(self, node, parent):
"""
Definition::
<any
id = ID
maxOccurs = (nonNegativeInteger | unbounded) : 1
@ -636,6 +807,12 @@ class SchemaVisitor(object):
{any attributes with non-schema Namespace...}>
Content: (annotation?)
</any>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
min_occurs, max_occurs = _process_occurs_attrs(node)
process_contents = node.get('processContents', 'strict')
@ -645,6 +822,8 @@ class SchemaVisitor(object):
def visit_sequence(self, node, parent):
"""
Definition::
<sequence
id = ID
maxOccurs = (nonNegativeInteger | unbounded) : 1
@ -653,6 +832,12 @@ class SchemaVisitor(object):
Content: (annotation?,
(element | group | choice | sequence | any)*)
</sequence>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
sub_types = [
@ -677,6 +862,8 @@ class SchemaVisitor(object):
"""Allows the elements in the group to appear (or not appear) in any
order in the containing element.
Definition::
<all
id = ID
maxOccurs= 1: 1
@ -684,6 +871,12 @@ class SchemaVisitor(object):
{any attributes with non-schema Namespace...}>
Content: (annotation?, element*)
</all>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
sub_types = [
@ -703,6 +896,8 @@ class SchemaVisitor(object):
"""Groups a set of element declarations so that they can be
incorporated as a group into complex type definitions.
Definition::
<group
name= NCName
id = ID
@ -714,9 +909,16 @@ class SchemaVisitor(object):
Content: (annotation?, (all | choice | sequence))
</group>
"""
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
result = self.process_reference(node)
"""
min_occurs, max_occurs = _process_occurs_attrs(node)
result = self.process_reference(
node, min_occurs=min_occurs, max_occurs=max_occurs)
if result:
return result
@ -730,11 +932,13 @@ class SchemaVisitor(object):
elm = xsd_elements.Group(name=qname, child=item)
if parent.tag == tags.schema:
self.document.register_group(qname, elm)
self.register_group(qname, elm)
return elm
def visit_list(self, node, parent):
"""
Definition::
<list
id = ID
itemType = QName
@ -745,6 +949,12 @@ class SchemaVisitor(object):
The use of the simpleType element child and the itemType attribute is
mutually exclusive.
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
item_type = qname_attr(node, 'itemType')
if item_type:
@ -757,6 +967,8 @@ class SchemaVisitor(object):
def visit_choice(self, node, parent):
"""
Definition::
<choice
id = ID
maxOccurs= (nonNegativeInteger | unbounded) : 1
@ -780,19 +992,27 @@ class SchemaVisitor(object):
def visit_union(self, node, parent):
"""Defines a collection of multiple simpleType definitions.
Definition::
<union
id = ID
memberTypes = List of QNames
{any attributes with non-schema Namespace}...>
Content: (annotation?, (simpleType*))
</union>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
# TODO
members = node.get('memberTypes')
types = []
if members:
for member in members.split():
qname = as_qname(member, node.nsmap, self.document._target_namespace)
qname = as_qname(member, node.nsmap)
xsd_type = self._get_type(qname)
types.append(xsd_type)
else:
@ -805,18 +1025,28 @@ class SchemaVisitor(object):
attribute or element values) must be unique within the specified scope.
The value must be unique or nil.
Definition::
<unique
id = ID
name = NCName
{any attributes with non-schema Namespace}...>
Content: (annotation?, (selector, field+))
</unique>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
# TODO
pass
def visit_attribute_group(self, node, parent):
"""
Definition::
<attributeGroup
id = ID
name = NCName
@ -825,6 +1055,12 @@ class SchemaVisitor(object):
Content: (annotation?),
((attribute | attributeGroup)*, anyAttribute?))
</attributeGroup>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
ref = self.process_reference(node)
if ref:
@ -835,10 +1071,12 @@ class SchemaVisitor(object):
attributes = self._process_attributes(node, children)
attribute_group = xsd_elements.AttributeGroup(qname, attributes)
self.document.register_attribute_group(qname, attribute_group)
self.register_attribute_group(qname, attribute_group)
def visit_any_attribute(self, node, parent):
"""
Definition::
<anyAttribute
id = ID
namespace = ((##any | ##other) |
@ -847,6 +1085,12 @@ class SchemaVisitor(object):
{any attributes with non-schema Namespace...}>
Content: (annotation?)
</anyAttribute>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
process_contents = node.get('processContents', 'strict')
return xsd_elements.AnyAttribute(process_contents=process_contents)
@ -856,6 +1100,8 @@ class SchemaVisitor(object):
non-XML data within an XML document. An XML Schema notation declaration
is a reconstruction of XML 1.0 NOTATION declarations.
Definition::
<notation
id = ID
name = NCName
@ -865,17 +1111,18 @@ class SchemaVisitor(object):
Content: (annotation?)
</notation>
:param node: The XML node
:type node: lxml.etree._Element
:param parent: The parent XML node
:type parent: lxml.etree._Element
"""
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
return UnresolvedType(name, self.schema)
def _create_qname(self, name):
if not isinstance(name, etree.QName):

View File

@ -1,6 +1,7 @@
import pytest
from pretend import stub
from lxml import etree
import aiohttp
from aioresponses import aioresponses
from zeep import cache, asyncio
@ -39,3 +40,21 @@ async def test_post(event_loop):
headers={})
assert result.content == b'x'
@pytest.mark.requests
@pytest.mark.asyncio
async def test_session_close(event_loop):
transport = asyncio.AsyncTransport(loop=event_loop)
session = transport.session # copy session object from transport
del transport
assert session.closed
@pytest.mark.requests
@pytest.mark.asyncio
async def test_session_no_close(event_loop):
session = aiohttp.ClientSession(loop=event_loop)
transport = asyncio.AsyncTransport(loop=event_loop, session=session)
del transport
assert not session.closed

View File

@ -11,6 +11,18 @@ def test_factory_namespace():
assert obj.NameLast == 'van Tellingen'
def test_factory_no_reference():
client = Client('tests/wsdl_files/soap.wsdl')
factory = client.type_factory('http://example.com/stockquote.xsd')
obj_1 = client.get_type('ns0:ArrayOfAddress')()
obj_1.Address.append({
'NameFirst': 'J',
'NameLast': 'Doe',
})
obj_2 = client.get_type('ns0:ArrayOfAddress')()
assert len(obj_2.Address) == 0
def test_factory_ns_auto_prefix():
client = Client('tests/wsdl_files/soap.wsdl')
factory = client.type_factory('ns0')

14
tests/test_loader.py Normal file
View File

@ -0,0 +1,14 @@
from zeep.loader import parse_xml
from tests.utils import DummyTransport
def test_huge_text():
# libxml2>=2.7.3 has XML_MAX_TEXT_LENGTH 10000000 without XML_PARSE_HUGE
tree = parse_xml(u"""
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<HugeText xmlns="http://hugetext">%s</HugeText>
</s:Body>
</s:Envelope>
""" % (u'\u00e5' * 10000001), DummyTransport(), xml_huge_tree=True)
assert tree[0][0].text == u'\u00e5' * 10000001

134
tests/test_soap_multiref.py Normal file
View File

@ -0,0 +1,134 @@
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 Client, wsdl
from zeep.transports import Transport
@pytest.mark.requests
def test_parse_soap_wsdl():
wsdl_file = io.StringIO(u"""
<?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/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
targetNamespace="http://tests.python-zeep.org/">
<wsdl:types>
<xsd:schema
targetNamespace="http://tests.python-zeep.org/"
xmlns:tns="http://tests.python-zeep.org/"
elementFormDefault="qualified">
<xsd:element name="input" type="xsd:string"/>
<xsd:element name="output">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="item_1" type="tns:type_1"/>
<xsd:element name="item_2" type="tns:type_2"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="type_1">
<xsd:sequence>
<xsd:element name="subitem_1" type="xsd:string"/>
<xsd:element name="subitem_2" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="type_2">
<xsd:sequence>
<xsd:element name="subitem_1" type="tns:type_1"/>
<xsd:element name="subitem_2" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
</wsdl:types>
<wsdl:message name="TestOperationRequest">
<wsdl:part name="response" element="tns:input"/>
</wsdl:message>
<wsdl:message name="TestOperationResponse">
<wsdl:part name="response" element="tns:output"/>
</wsdl:message>
<wsdl:portType name="TestPortType">
<wsdl:operation name="TestOperation">
<wsdl:input message="TestOperationRequest"/>
<wsdl:output message="TestOperationResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="TestBinding" type="tns:TestPortType">
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="TestOperation">
<soap:operation soapAction=""/>
<wsdl:input name="TestOperationRequest">
<soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
</wsdl:input>
<wsdl:output name="TestOperationResponse">
<soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
</wsdl:output>
</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())
content = """
<?xml version="1.0"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://tests.python-zeep.org/">
<soapenv:Body>
<tns:TestOperationResponse>
<tns:output>
<tns:item_1 href="#id0"/>
<tns:item_2>
<tns:subitem_1>
<tns:subitem_1>foo</tns:subitem_1>
<tns:subitem_2>bar</tns:subitem_2>
</tns:subitem_1>
<tns:subitem_2>bar</tns:subitem_2>
</tns:item_2>
</tns:output>
</tns:TestOperationResponse>
<multiRef id="id0">
<tns:subitem_1>foo</tns:subitem_1>
<tns:subitem_2>bar</tns:subitem_2>
</multiRef>
</soapenv:Body>
</soapenv:Envelope>
""".strip()
client = Client(wsdl_file, transport=Transport(),)
response = stub(
status_code=200,
headers={},
content=content)
operation = client.service._binding._operations['TestOperation']
result = client.service._binding.process_reply(
client, operation, response)
assert result.item_1.subitem_1 == 'foo'
assert result.item_1.subitem_2 == 'bar'
assert result.item_2.subitem_1.subitem_1 == 'foo'
assert result.item_2.subitem_1.subitem_2 == 'bar'
assert result.item_2.subitem_2 == 'bar'

View File

@ -100,7 +100,8 @@ def test_parse_soap_header_wsdl():
}
})
assert result == 120.123
assert result.body.price == 120.123
assert result.header.body is None
request = m.request_history[0]

View File

@ -1,12 +1,14 @@
import io
import pytest
from lxml import etree
from tests.utils import DummyTransport, assert_nodes_equal, load_xml
from tests.utils import DummyTransport, assert_nodes_equal, load_xml, render_node
from zeep import xsd
def get_transport():
@pytest.fixture(scope='function')
def transport():
transport = DummyTransport()
transport.bind(
'http://schemas.xmlsoap.org/soap/encoding/',
@ -14,7 +16,7 @@ def get_transport():
return transport
def test_simple_type():
def test_simple_type(transport):
schema = xsd.Schema(load_xml("""
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -30,7 +32,7 @@ def test_simple_type():
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>
"""), transport=get_transport())
"""), transport=transport)
ArrayOfString = schema.get_type('ns0:ArrayOfString')
print(ArrayOfString.__dict__)
@ -52,8 +54,105 @@ def test_simple_type():
assert_nodes_equal(expected, node)
data = ArrayOfString.parse_xmlelement(node, schema)
assert data == ['item', 'and', 'even', 'more', 'items']
assert data.as_value_object()
def test_complex_type():
def test_simple_type_nested(transport):
schema = xsd.Schema(load_xml("""
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
targetNamespace="http://tests.python-zeep.org/tns"
xmlns:tns="http://tests.python-zeep.org/tns">
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>
<xsd:complexType name="container">
<xsd:sequence>
<xsd:element name="strings" type="tns:ArrayOfString"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ArrayOfString">
<xsd:complexContent>
<xsd:restriction base="SOAP-ENC:Array">
<xsd:attribute ref="SOAP-ENC:arrayType" wsdl:arrayType="xsd:string[]"/>
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>
"""), transport=transport)
Container = schema.get_type('ns0:container')
value = Container(strings=['item', 'and', 'even', 'more', 'items'])
assert value.strings == ['item', 'and', 'even', 'more', 'items']
node = etree.Element('document')
Container.render(node, value)
expected = """
<document>
<strings>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">item</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">and</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">even</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">more</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">items</item>
</strings>
</document>
""" # noqa
assert_nodes_equal(expected, node)
data = Container.parse_xmlelement(node, schema)
assert data.strings == ['item', 'and', 'even', 'more', 'items']
def test_simple_type_nested_inline_type(transport):
schema = xsd.Schema(load_xml("""
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
targetNamespace="http://tests.python-zeep.org/tns"
xmlns:tns="http://tests.python-zeep.org/tns">
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>
<xsd:complexType name="container">
<xsd:sequence>
<xsd:element name="strings" type="tns:ArrayOfString"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ArrayOfString">
<xsd:complexContent>
<xsd:restriction base="SOAP-ENC:Array">
<xsd:attribute ref="SOAP-ENC:arrayType" wsdl:arrayType="xsd:string[]"/>
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>
"""), transport=transport)
Container = schema.get_type('ns0:container')
node = load_xml("""
<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<strings xsi:type="soapenc:Array" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">item</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">and</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">even</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">more</item>
<item xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">items</item>
</strings>
</document>
""") # noqa
data = Container.parse_xmlelement(node, schema)
assert data.strings == ['item', 'and', 'even', 'more', 'items']
def test_complex_type(transport):
schema = xsd.Schema(load_xml("""
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -80,7 +179,7 @@ def test_complex_type():
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>
"""), transport=get_transport())
"""), transport=transport)
ArrayOfObject = schema.get_type('ns0:ArrayOfObject')
ArrayObject = schema.get_type('ns0:ArrayObject')
@ -111,9 +210,17 @@ def test_complex_type():
</document>
"""
assert_nodes_equal(expected, node)
data = ArrayOfObject.parse_xmlelement(node, schema)
assert data[0].attr_1 == 'attr-1'
assert data[0].attr_2 == 'attr-2'
assert data[1].attr_1 == 'attr-3'
assert data[1].attr_2 == 'attr-4'
assert data[2].attr_1 == 'attr-5'
assert data[2].attr_2 == 'attr-6'
def test_complex_type_without_name():
def test_complex_type_without_name(transport):
schema = xsd.Schema(load_xml("""
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -137,7 +244,7 @@ def test_complex_type_without_name():
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>
"""), transport=get_transport())
"""), transport=transport)
ArrayOfObject = schema.get_type('ns0:ArrayOfObject')
ArrayObject = schema.get_type('ns0:ArrayObject')
@ -170,13 +277,13 @@ def test_complex_type_without_name():
assert_nodes_equal(expected, node)
data = ArrayOfObject.parse_xmlelement(node, schema)
assert len(data._value_1) == 3
assert data._value_1[0]['attr_1'] == 'attr-1'
assert data._value_1[0]['attr_2'] == 'attr-2'
assert data._value_1[1]['attr_1'] == 'attr-3'
assert data._value_1[1]['attr_2'] == 'attr-4'
assert data._value_1[2]['attr_1'] == 'attr-5'
assert data._value_1[2]['attr_2'] == 'attr-6'
assert len(data) == 3
assert data[0]['attr_1'] == 'attr-1'
assert data[0]['attr_2'] == 'attr-2'
assert data[1]['attr_1'] == 'attr-3'
assert data[1]['attr_2'] == 'attr-4'
assert data[2]['attr_1'] == 'attr-5'
assert data[2]['attr_2'] == 'attr-6'
def test_soap_array_parse_remote_ns():
@ -237,8 +344,8 @@ def test_soap_array_parse_remote_ns():
elm = schema.get_element('ns0:countries')
data = elm.parse(doc, schema)
assert data._value_1[0].code == 'NL'
assert data._value_1[0].name == 'The Netherlands'
assert data[0].code == 'NL'
assert data[0].name == 'The Netherlands'
def test_wsdl_array_type():
@ -284,9 +391,12 @@ def test_wsdl_array_type():
array = array_elm([item_1, item_2])
node = etree.Element('document')
assert array_elm.signature() == (
'_value_1: base[], arrayType: xsd:string, offset: arrayCoordinate, ' +
'id: xsd:ID, href: xsd:anyURI, _attr_1: {}')
assert array_elm.signature(schema=schema) == 'ns0:array(ns0:array)'
array_type = schema.get_type('ns0:array')
assert array_type.signature(schema=schema) == (
'ns0:array(_value_1: base[], arrayType: xsd:string, ' +
'offset: ns1:arrayCoordinate, id: xsd:ID, href: xsd:anyURI, _attr_1: {})')
array_elm.render(node, array)
expected = """
<document>
@ -363,7 +473,80 @@ def test_soap_array_parse():
elm = schema.get_element('ns0:FlagDetailsList')
data = elm.parse(doc, schema)
assert data.FlagDetailsStruct[0].Name == 'flag1'
assert data.FlagDetailsStruct[0].Value == 'value1'
assert data.FlagDetailsStruct[1].Name == 'flag2'
assert data.FlagDetailsStruct[1].Value == 'value2'
assert data[0].Name == 'flag1'
assert data[0].Value == 'value1'
assert data[1].Name == 'flag2'
assert data[1].Value == 'value2'
def test_xml_soap_enc_string(transport):
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
targetNamespace="http://tests.python-zeep.org/"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>
<xsd:element name="value" type="tns:ArrayOfString"/>
<xsd:complexType name="ArrayOfString">
<xsd:complexContent>
<xsd:restriction base="soapenc:Array">
<xsd:attribute ref="soapenc:arrayType" wsdl:arrayType="soapenc:string[]"/>
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
</xsd:schema>
"""), transport)
shoe_type = schema.get_element('{http://tests.python-zeep.org/}value')
obj = shoe_type(["foo"])
node = render_node(shoe_type, obj)
expected = """
<document>
<ns0:value xmlns:ns0="http://tests.python-zeep.org/">
<string>foo</string>
</ns0:value>
</document>
"""
assert_nodes_equal(expected, node)
obj = shoe_type.parse(node[0], schema)
assert obj[0]['_value_1'] == "foo"
# Via string-types
string_type = schema.get_type('{http://schemas.xmlsoap.org/soap/encoding/}string')
obj = shoe_type([string_type('foo')])
node = render_node(shoe_type, obj)
expected = """
<document>
<ns0:value xmlns:ns0="http://tests.python-zeep.org/">
<string>foo</string>
</ns0:value>
</document>
"""
assert_nodes_equal(expected, node)
obj = shoe_type.parse(node[0], schema)
assert obj[0]['_value_1'] == "foo"
# Via dicts
string_type = schema.get_type('{http://schemas.xmlsoap.org/soap/encoding/}string')
obj = shoe_type([{'_value_1': 'foo'}])
node = render_node(shoe_type, obj)
expected = """
<document>
<ns0:value xmlns:ns0="http://tests.python-zeep.org/">
<string>foo</string>
</ns0:value>
</document>
"""
assert_nodes_equal(expected, node)
obj = shoe_type.parse(node[0], schema)
assert obj[0]['_value_1'] == "foo"

View File

@ -54,14 +54,14 @@ def test_parse():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'xsd:string'
assert operation.input.header.signature() == ''
assert operation.input.envelope.signature() == 'body: xsd:string'
assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)'
assert operation.input.header.signature(schema=root.types) == 'soap-env:Header()'
assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(body: xsd:string)'
assert operation.input.signature(as_output=False) == 'xsd:string'
assert operation.output.body.signature() == 'xsd:string'
assert operation.output.header.signature() == ''
assert operation.output.envelope.signature() == 'body: xsd:string'
assert operation.output.body.signature(schema=root.types) == 'ns0:Response(xsd:string)'
assert operation.output.header.signature(schema=root.types) == 'soap-env:Header()'
assert operation.output.envelope.signature(schema=root.types) == 'soap-env:envelope(body: xsd:string)'
assert operation.output.signature(as_output=True) == 'xsd:string'
@ -111,9 +111,9 @@ def test_empty_input_parse():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == ''
assert operation.input.header.signature() == ''
assert operation.input.envelope.signature() == 'body: {}'
assert operation.input.body.signature(schema=root.types) == 'soap-env:Body()'
assert operation.input.header.signature(schema=root.types) == 'soap-env:Header()'
assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(body: {})'
assert operation.input.signature(as_output=False) == ''
@ -171,15 +171,15 @@ def test_parse_with_header():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'xsd:string'
assert operation.input.header.signature() == 'auth: RequestHeader()'
assert operation.input.envelope.signature() == 'body: xsd:string, header: {auth: RequestHeader()}' # noqa
assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: RequestHeader()}' # noqa
assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)'
assert operation.input.header.signature(schema=root.types) == 'soap-env:Header(auth: xsd:string)'
assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: xsd:string}, body: xsd:string)' # noqa
assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: xsd:string}' # noqa
assert operation.output.body.signature() == 'xsd:string'
assert operation.output.header.signature() == 'auth: ResponseHeader()'
assert operation.output.envelope.signature() == 'body: xsd:string, header: {auth: ResponseHeader()}' # noqa
assert operation.output.signature(as_output=True) == 'body: xsd:string, header: {auth: ResponseHeader()}' # noqa
assert operation.output.body.signature(schema=root.types) == 'ns0:Response(xsd:string)'
assert operation.output.header.signature(schema=root.types) == 'soap-env:Header(auth: xsd:string)'
assert operation.output.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: xsd:string}, body: xsd:string)' # noqa
assert operation.output.signature(as_output=True) == 'header: {auth: xsd:string}, body: xsd:string' # noqa
def test_parse_with_header_type():
@ -240,15 +240,15 @@ def test_parse_with_header_type():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'xsd:string'
assert operation.input.header.signature() == 'auth: RequestHeaderType'
assert operation.input.envelope.signature() == 'body: xsd:string, header: {auth: RequestHeaderType}' # noqa
assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: RequestHeaderType}' # noqa
assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)'
assert operation.input.header.signature(schema=root.types) == 'soap-env:Header(auth: ns0:RequestHeaderType)'
assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: ns0:RequestHeaderType}, body: xsd:string)' # noqa
assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: ns0:RequestHeaderType}' # noqa
assert operation.output.body.signature() == 'xsd:string'
assert operation.output.header.signature() == 'auth: ResponseHeaderType'
assert operation.output.envelope.signature() == 'body: xsd:string, header: {auth: ResponseHeaderType}' # noqa
assert operation.output.signature(as_output=True) == 'body: xsd:string, header: {auth: ResponseHeaderType}' # noqa
assert operation.output.body.signature(schema=root.types) == 'ns0:Response(xsd:string)'
assert operation.output.header.signature(schema=root.types) == 'soap-env:Header(auth: ns0:ResponseHeaderType)'
assert operation.output.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: ns0:ResponseHeaderType}, body: xsd:string)' # noqa
assert operation.output.signature(as_output=True) == 'header: {auth: ns0:ResponseHeaderType}, body: xsd:string' # noqa
def test_parse_with_header_other_message():
@ -292,12 +292,13 @@ def test_parse_with_header_other_message():
""".strip())
root = wsdl.Document(wsdl_content, None)
root.types.set_ns_prefix('soap-env', 'http://schemas.xmlsoap.org/soap/envelope/')
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.header.signature() == 'header: RequestHeader()'
assert operation.input.body.signature() == 'xsd:string'
assert operation.input.header.signature(schema=root.types) == 'soap-env:Header(header: xsd:string)'
assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)'
header = root.types.get_element(
'{http://tests.python-zeep.org/tns}RequestHeader'
@ -1193,7 +1194,7 @@ def test_deserialize_with_headers():
serialized = operation.process_reply(response_body)
assert operation.output.signature(as_output=True) == (
'body: {request_1: Request1(), request_2: Request2()}, header: {header_1: Header1(), header_2: Header2()}') # noqa
'header: {header_1: ns0:Header1, header_2: xsd:string}, body: {request_1: ns0:Request1, request_2: ns0:Request2}')
assert serialized.body.request_1.arg1 == 'ah1'
assert serialized.body.request_2.arg2 == 'ah2'
assert serialized.header.header_1.username == 'mvantellingen'

View File

@ -52,10 +52,10 @@ def test_urlencoded_serialize():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string'
assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)'
assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string'
assert operation.output.body.signature() == 'Body: xsd:string'
assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)'
assert operation.output.signature(as_output=True) == 'xsd:string'
serialized = operation.input.serialize(arg1='ah1', arg2='ah2')
@ -112,10 +112,10 @@ def test_urlreplacement_serialize():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string'
assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)'
assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string'
assert operation.output.body.signature() == 'Body: xsd:string'
assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)'
assert operation.output.signature(as_output=True) == 'xsd:string'
serialized = operation.input.serialize(arg1='ah1', arg2='ah2')
@ -172,10 +172,10 @@ def test_mime_content_serialize_form_urlencoded():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string'
assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)'
assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string'
assert operation.output.body.signature() == 'Body: xsd:string'
assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)'
assert operation.output.signature(as_output=True) == 'xsd:string'
serialized = operation.input.serialize(arg1='ah1', arg2='ah2')
@ -229,10 +229,10 @@ def test_mime_content_serialize_text_xml():
binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding']
operation = binding.get('TestOperation')
assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string'
assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)'
assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string'
assert operation.output.body.signature() == 'Body: xsd:string'
assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)'
assert operation.output.signature(as_output=True) == 'xsd:string'
serialized = operation.input.serialize(arg1='ah1', arg2='ah2')

View File

@ -1,12 +1,36 @@
# -*- coding: utf-8 -*-
import pytest
from lxml import etree
from pretend import stub
from tests.utils import load_xml
from zeep import Client
from zeep.exceptions import Fault
from zeep.exceptions import TransportError
from zeep.wsdl import bindings
def test_soap11_no_output():
client = Client('tests/wsdl_files/soap.wsdl')
content = """
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:stoc="http://example.com/stockquote.xsd">
<soapenv:Body></soapenv:Body>
</soapenv:Envelope>
""".strip()
response = stub(
status_code=200,
headers={},
content=content)
operation = client.service._binding._operations['GetLastTradePriceNoOutput']
res = client.service._binding.process_reply(client, operation, response)
assert res is None
def test_soap11_process_error():
response = load_xml("""
<soapenv:Envelope
@ -26,10 +50,10 @@ def test_soap11_process_error():
</soapenv:Body>
</soapenv:Envelope>
""")
binding = bindings.Soap11Binding(
wsdl=None, name=None, port_name=None, transport=None,
default_style=None)
try:
binding.process_error(response, None)
assert False
@ -112,6 +136,79 @@ def test_soap12_process_error():
assert exc.subcodes[1].localname == 'fault-subcode2'
def test_no_content_type():
data = """
<?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 = Client('tests/wsdl_files/soap.wsdl')
binding = client.service._binding
response = stub(
status_code=200,
content=data,
encoding='utf-8',
headers={}
)
result = binding.process_reply(
client, binding.get('GetLastTradePrice'), response)
assert result == 120.123
def test_wrong_content():
data = """
The request is answered something unexpected,
like an html page or a raw internal stack trace
""".strip()
client = Client('tests/wsdl_files/soap.wsdl')
binding = client.service._binding
response = stub(
status_code=200,
content=data,
encoding='utf-8',
headers={}
)
with pytest.raises(TransportError):
binding.process_reply(
client, binding.get('GetLastTradePrice'), response)
def test_wrong_no_unicode_content():
data = """
The request is answered something unexpected,
and the content charset is beyond unicode òñÇÿ
""".strip()
client = Client('tests/wsdl_files/soap.wsdl')
binding = client.service._binding
response = stub(
status_code=200,
content=data,
encoding='utf-8',
headers={}
)
with pytest.raises(TransportError):
binding.process_reply(
client, binding.get('GetLastTradePrice'), response)
def test_mime_multipart():
data = '\r\n'.join(line.strip() for line in """
--MIME_boundary
@ -154,6 +251,7 @@ def test_mime_multipart():
response = stub(
status_code=200,
content=data,
encoding='utf-8',
headers={
'Content-Type': 'multipart/related; type="text/xml"; start="<claim061400a.xml@claiming-it.com>"; boundary="MIME_boundary"'
}
@ -167,3 +265,95 @@ def test_mime_multipart():
assert result.attachments[0].content == b'...Base64 encoded TIFF image...'
assert result.attachments[1].content == b'...Raw JPEG image..'
def test_mime_multipart_no_encoding():
data = '\r\n'.join(line.strip() for line in """
--MIME_boundary
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
Content-ID: <claim061400a.xml@claiming-it.com>
<?xml version='1.0' ?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<claim:insurance_claim_auto id="insurance_claim_document_id"
xmlns:claim="http://schemas.risky-stuff.com/Auto-Claim">
<theSignedForm href="cid:claim061400a.tiff@claiming-it.com"/>
<theCrashPhoto href="cid:claim061400a.jpeg@claiming-it.com"/>
<!-- ... more claim details go here... -->
</claim:insurance_claim_auto>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
--MIME_boundary
Content-Type: image/tiff
Content-Transfer-Encoding: base64
Content-ID: <claim061400a.tiff@claiming-it.com>
Li4uQmFzZTY0IGVuY29kZWQgVElGRiBpbWFnZS4uLg==
--MIME_boundary
Content-Type: text/xml
Content-ID: <claim061400a.jpeg@claiming-it.com>
...Raw JPEG image..
--MIME_boundary--
""".splitlines()).encode('utf-8')
client = Client('tests/wsdl_files/claim.wsdl')
binding = client.service._binding
response = stub(
status_code=200,
content=data,
encoding=None,
headers={
'Content-Type': 'multipart/related; type="text/xml"; start="<claim061400a.xml@claiming-it.com>"; boundary="MIME_boundary"'
}
)
result = binding.process_reply(
client, binding.get('GetClaimDetails'), response)
assert result.root is None
assert len(result.attachments) == 2
assert result.attachments[0].content == b'...Base64 encoded TIFF image...'
assert result.attachments[1].content == b'...Raw JPEG image..'
def test_unexpected_headers():
data = """
<?xml version="1.0"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:stoc="http://example.com/stockquote.xsd">
<soapenv:Header>
<stoc:IamUnexpected>uhoh</stoc:IamUnexpected>
</soapenv:Header>
<soapenv:Body>
<stoc:TradePrice>
<price>120.123</price>
</stoc:TradePrice>
</soapenv:Body>
</soapenv:Envelope>
""".strip()
client = Client('tests/wsdl_files/soap_header.wsdl')
binding = client.service._binding
response = stub(
status_code=200,
content=data,
encoding='utf-8',
headers={}
)
result = binding.process_reply(
client, binding.get('GetLastTradePrice'), response)
assert result.body.price == 120.123
assert result.header.body is None
assert len(result.header._raw_elements) == 1

View File

@ -149,39 +149,6 @@ def test_invalid_kwarg_simple_type():
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():
@ -515,7 +482,7 @@ def test_duplicate_element_names():
))
# sequences
expected = 'item: xsd:string, item__1: xsd:string, item__2: xsd:string'
expected = '{http://tests.python-zeep.org/}container(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')
@ -548,7 +515,7 @@ def test_element_attribute_name_conflict():
))
# sequences
expected = 'item: xsd:string, foo: xsd:string, attr__item: xsd:string'
expected = '{http://tests.python-zeep.org/}container(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')

View File

@ -3,7 +3,7 @@ import datetime
import pytest
from lxml import etree
from tests.utils import assert_nodes_equal, load_xml
from tests.utils import assert_nodes_equal, load_xml, render_node
from zeep import xsd
@ -26,6 +26,24 @@ def get_any_schema():
"""))
def test_default_xsd_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"/>
</schema>
"""))
assert schema
container_cls = schema.get_element('ns0:container')
data = container_cls()
assert data == ''
def test_any_simple():
schema = get_any_schema()
@ -109,6 +127,41 @@ def test_any_value_invalid():
container_elm.render(node, obj)
def test_any_without_element():
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="item">
<complexType>
<sequence>
<any minOccurs="0" maxOccurs="1"/>
</sequence>
<attribute name="type" type="string"/>
<attribute name="title" type="string"/>
</complexType>
</element>
</schema>
"""))
item_elm = schema.get_element('{http://tests.python-zeep.org/}item')
item = item_elm(xsd.AnyObject(xsd.String(), 'foobar'), type='attr-1', title='attr-2')
node = render_node(item_elm, item)
expected = """
<document>
<ns0:item xmlns:ns0="http://tests.python-zeep.org/" type="attr-1" title="attr-2">foobar</ns0:item>
</document>
"""
assert_nodes_equal(expected, node)
item = item_elm.parse(node.getchildren()[0], schema)
assert item.type == 'attr-1'
assert item.title == 'attr-2'
assert item._value_1 is None
def test_any_with_ref():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
@ -219,6 +272,36 @@ def test_element_any_type():
item = container_elm.parse(node.getchildren()[0], schema)
assert item.something == 'bar'
def test_element_any_type_unknown_type():
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="container">
<complexType>
<sequence>
<element name="something" />
</sequence>
</complexType>
</element>
</schema>
""".strip())
schema = xsd.Schema(node)
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
node = load_xml("""
<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ns0:container xmlns:ns0="http://tests.python-zeep.org/">
<ns0:something xmlns:q1="http://microsoft.com/wsdl/types/" xsi:type="q1:guid">bar</ns0:something>
</ns0:container>
</document>
""")
item = container_elm.parse(node.getchildren()[0], schema)
assert item.something == 'bar'
def test_element_any_type_elements():
node = etree.fromstring("""
@ -298,8 +381,8 @@ def test_any_in_nested_sequence():
""")) # noqa
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
assert container_elm.signature() == (
'items: {_value_1: ANY}, version: xsd:string, _value_1: ANY[]')
assert container_elm.signature(schema) == (
'ns0:container(items: {_value_1: ANY}, version: xsd:string, _value_1: ANY[])')
something = schema.get_element('{http://tests.python-zeep.org/}something')
foobar = schema.get_element('{http://tests.python-zeep.org/}foobar')

View File

@ -26,8 +26,8 @@ def test_anyattribute():
"""))
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
assert container_elm.signature() == (
'foo: xsd:string, _attr_1: {}')
assert container_elm.signature(schema) == (
'ns0:container(foo: xsd:string, _attr_1: {})')
obj = container_elm(foo='bar', _attr_1=OrderedDict([
('hiep', 'hoi'), ('hoi', 'hiep')
]))
@ -73,7 +73,8 @@ def test_attribute_list_type():
"""))
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
assert container_elm.signature() == ('foo: xsd:string, lijst: xsd:int[]')
assert container_elm.signature(schema) == (
'ns0:container(foo: xsd:string, lijst: xsd:int[])')
obj = container_elm(foo='bar', lijst=[1, 2, 3])
expected = """
<document>
@ -341,7 +342,8 @@ def test_nested_attribute():
"""))
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
assert container_elm.signature() == 'item: {x: xsd:string, y: xsd:string}'
assert container_elm.signature(schema) == (
'ns0:container(item: {x: xsd:string, y: xsd:string})')
obj = container_elm(item={'x': 'foo', 'y': 'bar'})
expected = """
@ -371,7 +373,7 @@ def test_attribute_union_type():
<restriction base="xsd:string"/>
</simpleType>
<simpleType name="parent">
<union memberTypes="one two"/>
<union memberTypes="tns:one tns:two"/>
</simpleType>
<simpleType name="two">
<restriction base="xsd:string"/>

View File

@ -30,6 +30,8 @@ class TestBoolean:
assert instance.xmlvalue(False) == 'false'
assert instance.xmlvalue(1) == 'true'
assert instance.xmlvalue(0) == 'false'
assert instance.xmlvalue('false') == 'false'
assert instance.xmlvalue('0') == 'false'
def test_pythonvalue(self):
instance = builtins.Boolean()

View File

@ -1,11 +1,11 @@
import pytest
from lxml import etree
import pytest
from tests.utils import assert_nodes_equal, load_xml, render_node
from zeep import xsd
def test_single_node():
def test_xml_xml_single_node():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -40,7 +40,7 @@ def test_single_node():
assert obj.item == 'bar'
def test_nested_sequence():
def test_xml_nested_sequence():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -86,7 +86,37 @@ def test_nested_sequence():
assert obj.item.y == 2
def test_single_node_array():
def test_xml_restriction_self():
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" type="tns:container"/>
<complexType name="container">
<complexContent>
<restriction base="anyType">
<sequence>
<element minOccurs="0" maxOccurs="1" name="item">
<complexType>
<sequence>
<element name="child" type="tns:container"/>
</sequence>
</complexType>
</element>
</sequence>
</restriction>
</complexContent>
</complexType>
</schema>
"""))
schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/')
container_elm = schema.get_element('tns:container')
container_elm.signature(schema)
def test_xml_single_node_array():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -124,7 +154,7 @@ def test_single_node_array():
assert obj.item == ['item-1', 'item-2', 'item-3']
def test_single_node_no_iterable():
def test_xml_single_node_no_iterable():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -151,7 +181,7 @@ def test_single_node_no_iterable():
render_node(container_elm, obj)
def test_complex_any_types():
def test_xml_complex_any_types():
# see https://github.com/mvantellingen/python-zeep/issues/252
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
@ -231,3 +261,37 @@ def test_complex_any_types():
</document>
""") # noqa
assert_nodes_equal(result, expected)
def test_xml_unparsed_elements():
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" type="string" />
</sequence>
</complexType>
</element>
</schema>
"""))
schema.strict = False
schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/')
expected = load_xml("""
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item>bar</ns0:item>
<ns0:idontbelonghere>bar</ns0:idontbelonghere>
</ns0:container>
</document>
""")
container_elm = schema.get_element('tns:container')
obj = container_elm.parse(expected[0], schema)
assert obj.item == 'bar'
assert obj._raw_elements

View File

@ -123,7 +123,8 @@ def test_complex_content_with_recursive_elements():
</xsd:schema>
"""))
pet_type = schema.get_element('{http://tests.python-zeep.org/}Pet')
assert(pet_type.signature() == 'name: xsd:string, common_name: xsd:string, children: Pet')
assert(pet_type.signature(schema=schema) == 'ns0:Pet(ns0:Pet)')
assert(pet_type.type.signature(schema=schema) == 'ns0:Pet(name: xsd:string, common_name: xsd:string, children: ns0:Pet[])')
obj = pet_type(
name='foo', common_name='bar',
@ -602,3 +603,100 @@ def test_extension_abstract_complex_type():
</document>
"""
assert_nodes_equal(expected, node)
def test_extension_base_anytype():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<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:complexContent>
<xsd:restriction base="xsd:anyType">
<xsd:attribute name="attr" type="xsd:unsignedInt" use="required"/>
<xsd:anyAttribute namespace="##other" processContents="lax"/>
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""))
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
assert container_elm.signature() == (
'{http://tests.python-zeep.org/}container(attr: xsd:unsignedInt, _attr_1: {})')
obj = container_elm(attr='foo')
node = render_node(container_elm, obj)
expected = """
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/" attr="foo"/>
</document>
"""
assert_nodes_equal(expected, node)
def test_extension_on_ref():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<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:complexType name="type">
<xsd:complexContent>
<xsd:extension base="tns:base">
<xsd:sequence>
<xsd:element ref="tns:extra"/>
</xsd:sequence>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="base">
<xsd:sequence>
<xsd:element name="item-1" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="extra" type="xsd:string"/>
</xsd:schema>
"""))
type_cls = schema.get_type('ns0:type')
assert type_cls.signature()
def test_restrict_on_ref():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<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:complexType name="type">
<xsd:complexContent>
<xsd:restriction base="tns:base">
<xsd:sequence>
<xsd:element ref="tns:extra"/>
</xsd:sequence>
</xsd:restriction>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="base">
<xsd:sequence>
<xsd:element name="item-1" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="extra" type="xsd:string"/>
</xsd:schema>
"""))
type_cls = schema.get_type('ns0:type')
assert type_cls.signature()

View File

@ -0,0 +1,65 @@
from lxml import etree
from tests.utils import assert_nodes_equal, render_node, load_xml
from zeep import xsd
def test_build_occurs_1():
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()),
])
))
obj = custom_type(item_1='foo', item_2='bar')
result = render_node(custom_type, obj)
expected = load_xml("""
<document>
<ns0:authentication xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_1>foo</ns0:item_1>
<ns0:item_2>bar</ns0:item_2>
</ns0:authentication>
</document>
""")
assert_nodes_equal(result, expected)
obj = custom_type.parse(result[0], None)
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'
def test_build_pare_other_order():
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()),
])
))
xml = load_xml("""
<document>
<ns0:authentication xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_2>bar</ns0:item_2>
<ns0:item_1>foo</ns0:item_1>
</ns0:authentication>
</document>
""")
obj = custom_type.parse(xml[0], None)
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'

View File

@ -1,9 +1,10 @@
from collections import deque
import pytest
from lxml import etree
from tests.utils import assert_nodes_equal, load_xml, render_node
from zeep import xsd
from zeep.exceptions import XMLParseError
from zeep.exceptions import XMLParseError, ValidationError
from zeep.helpers import serialize_object
@ -45,7 +46,7 @@ def test_choice_element():
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node.getchildren()[0], schema)
value = element.parse(node[0], schema)
assert value.item_1 == 'foo'
assert value.item_2 is None
assert value.item_3 is None
@ -89,11 +90,89 @@ def test_choice_element_second_elm():
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node.getchildren()[0], schema)
value = element.parse(node[0], schema)
assert value.item_1 is None
assert value.item_2 == 'foo'
assert value.item_3 is None
def test_choice_element_second_elm_positional():
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:complexType name="type_1">
<xsd:sequence>
<xsd:element name="child_1" type="xsd:string"/>
<xsd:element name="child_2" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="type_2">
<xsd:sequence>
<xsd:element name="child_1" type="xsd:string"/>
<xsd:element name="child_2" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="container">
<xsd:complexType>
<xsd:choice>
<xsd:element name="item_1" type="tns:type_1" />
<xsd:element name="item_2" type="tns:type_2" />
</xsd:choice>
</xsd:complexType>
</xsd:element>
<xsd:element name="containerArray">
<xsd:complexType>
<xsd:sequence>
<xsd:choice>
<xsd:element name="item_1" type="tns:type_1" />
<xsd:element name="item_2" type="tns:type_2" />
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
""".strip())
schema = xsd.Schema(node)
child = schema.get_type('ns0:type_2')(child_1='ha', child_2='ho')
element = schema.get_element('ns0:container')
with pytest.raises(TypeError):
value = element(child)
value = element(item_2=child)
element = schema.get_element('ns0:containerArray')
with pytest.raises(TypeError):
value = element(child)
value = element(item_2=child)
element = schema.get_element('ns0:container')
value = element(item_2=child)
assert value.item_1 is None
assert value.item_2 == child
expected = """
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_2>
<ns0:child_1>ha</ns0:child_1>
<ns0:child_2>ho</ns0:child_2>
</ns0:item_2>
</ns0:container>
</document>
"""
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
assert value.item_1 is None
assert value.item_2.child_1 == 'ha'
assert value.item_2.child_2 == 'ho'
def test_choice_element_multiple():
node = etree.fromstring("""
@ -137,7 +216,7 @@ def test_choice_element_multiple():
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node.getchildren()[0], schema)
value = element.parse(node[0], schema)
assert value._value_1 == [
{'item_1': 'foo'}, {'item_2': 'bar'}, {'item_1': 'three'},
]
@ -179,6 +258,8 @@ def test_choice_element_optional():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
assert value.item_4 == 'foo'
def test_choice_element_with_any():
@ -219,7 +300,7 @@ def test_choice_element_with_any():
element.render(node, value)
assert_nodes_equal(expected, node)
result = element.parse(node.getchildren()[0], schema)
result = element.parse(node[0], schema)
assert result.name == 'foo'
assert result.something is True
assert result.item_1 == 'foo'
@ -266,7 +347,7 @@ def test_choice_element_with_any_max_occurs():
"""
node = render_node(element, value)
assert_nodes_equal(node, expected)
result = element.parse(node.getchildren()[0], schema)
result = element.parse(node[0], schema)
assert result.item_2 == 'item-2'
assert result._value_1 == ['any-content']
@ -319,8 +400,8 @@ def test_choice_in_sequence():
schema = xsd.Schema(node)
container_elm = schema.get_element('ns0:container')
assert container_elm.type.signature() == (
'something: xsd:string, ({item_1: xsd:string} | {item_2: xsd:string} | {item_3: xsd:string})') # noqa
assert container_elm.type.signature(schema=schema) == (
'ns0:container(something: xsd:string, ({item_1: xsd:string} | {item_2: xsd:string} | {item_3: xsd:string}))')
value = container_elm(something='foobar', item_1='item-1')
expected = """
@ -334,6 +415,7 @@ def test_choice_in_sequence():
node = etree.Element('document')
container_elm.render(node, value)
assert_nodes_equal(expected, node)
value = container_elm.parse(node[0], schema)
def test_choice_with_sequence():
@ -362,8 +444,8 @@ def test_choice_with_sequence():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})')
assert element.type.signature(schema=schema) == (
'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string}))')
value = element(item_1='foo', item_2='bar')
expected = """
@ -377,6 +459,7 @@ def test_choice_with_sequence():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_once():
@ -404,8 +487,8 @@ def test_choice_with_sequence_once():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string})')
assert element.type.signature(schema=schema) == (
'ns0:container(item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string}))')
value = element(item_0='nul', item_1='foo', item_2='bar')
expected = """
@ -420,6 +503,94 @@ def test_choice_with_sequence_once():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_unbounded():
node = load_xml("""
<?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 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:choice>
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="item_0" type="xsd:string"/>
<xsd:element name="item_1" type="xsd:string"/>
<xsd:element name="item_2" type="tns:obj"/>
</xsd:sequence>
</xsd:choice>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="obj">
<xsd:sequence maxOccurs="unbounded">
<xsd:element name="item_2_1" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature(schema=schema) == (
'ns0:container(({[item_0: xsd:string, item_1: xsd:string, item_2: ns0:obj]}))')
value = element(_value_1=[
{'item_0': 'nul', 'item_1': 'foo', 'item_2': {'_value_1': [{'item_2_1': 'bar'}]}},
])
expected = """
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_0>nul</ns0:item_0>
<ns0:item_1>foo</ns0:item_1>
<ns0:item_2>
<ns0:item_2_1>bar</ns0:item_2_1>
</ns0:item_2>
</ns0:container>
</document>
"""
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
assert value._value_1[0]['item_0'] == 'nul'
assert value._value_1[0]['item_1'] == 'foo'
assert value._value_1[0]['item_2']._value_1[0]['item_2_1'] == 'bar'
assert not hasattr(value._value_1[0]['item_2'], 'item_2_1')
def test_choice_with_sequence_missing_elements():
node = load_xml("""
<?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 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:choice maxOccurs="2">
<xsd:sequence>
<xsd:element name="item_1" type="xsd:string"/>
<xsd:element name="item_2" type="xsd:string"/>
</xsd:sequence>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature(schema=schema) == (
'ns0:container(({item_1: xsd:string, item_2: xsd:string})[])')
value = element(_value_1={'item_1': 'foo'})
with pytest.raises(ValidationError):
render_node(element, value)
def test_choice_with_sequence_once_extra_data():
@ -448,8 +619,8 @@ def test_choice_with_sequence_once_extra_data():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string}), item_3: xsd:string')
assert element.type.signature(schema=schema) == (
'ns0:container(item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string}), item_3: xsd:string)')
value = element(item_0='nul', item_1='foo', item_2='bar', item_3='item-3')
expected = """
@ -465,6 +636,7 @@ def test_choice_with_sequence_once_extra_data():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_second():
@ -493,8 +665,8 @@ def test_choice_with_sequence_second():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})')
assert element.type.signature(schema=schema) == (
'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string}))')
value = element(item_3='foo', item_4='bar')
expected = """
@ -508,6 +680,7 @@ def test_choice_with_sequence_second():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_invalid():
@ -536,8 +709,8 @@ def test_choice_with_sequence_invalid():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})')
assert element.type.signature(schema=schema) == (
'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string}))')
with pytest.raises(TypeError):
element(item_1='foo', item_4='bar')
@ -594,6 +767,7 @@ def test_choice_with_sequence_change():
node = etree.Element('document')
element.render(node, elm)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_change_named():
@ -638,6 +812,7 @@ def test_choice_with_sequence_change_named():
node = etree.Element('document')
element.render(node, elm)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_multiple():
@ -666,8 +841,8 @@ def test_choice_with_sequence_multiple():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})[]')
assert element.type.signature(schema=schema) == (
'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})[])')
value = element(_value_1=[
dict(item_1='foo', item_2='bar'),
dict(item_3='foo', item_4='bar'),
@ -686,6 +861,7 @@ def test_choice_with_sequence_multiple():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_choice_with_sequence_and_element():
@ -713,8 +889,8 @@ def test_choice_with_sequence_and_element():
""")
schema = xsd.Schema(node)
element = schema.get_element('ns0:container')
assert element.type.signature() == (
'({item_1: xsd:string} | {({item_2: xsd:string} | {item_3: xsd:string})})')
assert element.type.signature(schema=schema) == (
'ns0:container(({item_1: xsd:string} | {({item_2: xsd:string} | {item_3: xsd:string})}))')
value = element(item_2='foo')
@ -728,6 +904,7 @@ def test_choice_with_sequence_and_element():
node = etree.Element('document')
element.render(node, value)
assert_nodes_equal(expected, node)
value = element.parse(node[0], schema)
def test_element_ref_in_choice():
@ -1043,3 +1220,138 @@ def test_choice_extend():
assert value['item-1-2'] == 'bar'
assert value['_value_1'][0] == {'item-2-1': 'xafoo'}
assert value['_value_1'][1] == {'item-2-2': 'xabar'}
def test_nested_choice():
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>
<choice>
<choice minOccurs="2" maxOccurs="unbounded">
<element ref="tns:a" />
</choice>
<element ref="tns:b" />
</choice>
</sequence>
</complexType>
</element>
<element name="a" type="string" />
<element name="b" type="string" />
</schema>
"""))
schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/')
container_type = schema.get_element('tns:container')
item = container_type(_value_1=[{'a': 'item-1'}, {'a': 'item-2'}])
assert item._value_1[0] == {'a': 'item-1'}
assert item._value_1[1] == {'a': 'item-2'}
expected = load_xml("""
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/">
<ns0:a>item-1</ns0:a>
<ns0:a>item-2</ns0:a>
</ns0:container>
</document>
""")
node = render_node(container_type, item)
assert_nodes_equal(node, expected)
result = container_type.parse(expected[0], schema)
assert result._value_1[0] == {'a': 'item-1'}
assert result._value_1[1] == {'a': 'item-2'}
expected = load_xml("""
<container xmlns="http://tests.python-zeep.org/">
<b>1</b>
</container>
""")
result = container_type.parse(expected, schema)
assert result.b == '1'
def test_unit_choice_parse_xmlelements_max_1():
schema = xsd.Schema(load_xml("""
<?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:choice>
<xsd:element name="item_1" type="xsd:string" />
<xsd:element name="item_2" type="xsd:string" />
<xsd:element name="item_3" type="xsd:string" />
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""))
element = schema.get_element('ns0:container')
def create_elm(name, text):
elm = etree.Element(name)
elm.text = text
return elm
data = deque([
create_elm('item_1', 'item-1'),
create_elm('item_2', 'item-2'),
create_elm('item_1', 'item-3'),
])
result = element.type._element.parse_xmlelements(data, schema)
assert result == {'item_1': 'item-1'}
assert len(data) == 2
def test_unit_choice_parse_xmlelements_max_2():
schema = xsd.Schema(load_xml("""
<?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:choice maxOccurs="2">
<xsd:element name="item_1" type="xsd:string" />
<xsd:element name="item_2" type="xsd:string" />
<xsd:element name="item_3" type="xsd:string" />
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""))
element = schema.get_element('ns0:container')
def create_elm(name, text):
elm = etree.Element(name)
elm.text = text
return elm
data = deque([
create_elm('item_1', 'item-1'),
create_elm('item_2', 'item-2'),
create_elm('item_1', 'item-3'),
])
result = element.type._element.parse_xmlelements(data, schema, name='items')
assert result == {
'items': [
{'item_1': 'item-1'},
{'item_2': 'item-2'},
]
}
assert len(data) == 1

View File

@ -0,0 +1,417 @@
import pytest
from lxml import etree
from tests.utils import assert_nodes_equal, render_node, load_xml
from zeep import xsd
def test_build_objects():
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)
obj = custom_type.parse(node[0], None)
assert obj.username == 'foo'
assert obj.password == 'bar'
def test_build_group_min_occurs_1():
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)
))
obj = custom_type(item_1='foo', item_2='bar')
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'
result = render_node(custom_type, obj)
expected = load_xml("""
<document>
<ns0:authentication xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_1>foo</ns0:item_1>
<ns0:item_2>bar</ns0:item_2>
</ns0:authentication>
</document>
""")
assert_nodes_equal(result, expected)
obj = custom_type.parse(result[0], None)
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'
assert not hasattr(obj, 'foobar')
def test_build_group_min_occurs_1_parse_args():
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)
))
obj = custom_type('foo', 'bar')
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'
def test_build_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)
))
obj = custom_type(_value_1=[
{'item_1': 'foo', 'item_2': 'bar'},
{'item_1': 'foo', 'item_2': 'bar'},
])
assert obj._value_1 == [
{'item_1': 'foo', 'item_2': 'bar'},
{'item_1': 'foo', 'item_2': 'bar'},
]
result = render_node(custom_type, obj)
expected = load_xml("""
<document>
<ns0:authentication 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:authentication>
</document>
""")
assert_nodes_equal(result, expected)
obj = custom_type.parse(result[0], None)
assert obj._value_1 == [
{'item_1': 'foo', 'item_2': 'bar'},
{'item_1': 'foo', 'item_2': 'bar'},
]
assert not hasattr(obj, 'foobar')
def test_build_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_build_group_occurs_1_invalid_kwarg():
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, max_occurs=1)
))
with pytest.raises(TypeError):
custom_type(item_1='foo', item_2='bar', error=True)
def test_build_group_min_occurs_2_invalid_kwarg():
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)
))
with pytest.raises(TypeError):
custom_type(_value_1=[
{'item_1': 'foo', 'item_2': 'bar', 'error': True},
{'item_1': 'foo', 'item_2': 'bar'},
])
def test_xml_group_via_ref():
schema = xsd.Schema(load_xml("""
<?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>
"""))
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_xml_group_via_ref_max_occurs_unbounded():
schema = xsd.Schema(load_xml("""
<?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" minOccurs="0" maxOccurs="unbounded"/>
</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>
"""))
address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
obj = address_type(
_value_1=[
{'first_name': 'foo-1', 'last_name': 'bar-1'},
{'first_name': 'foo-2', 'last_name': 'bar-2'},
])
node = etree.Element('document')
address_type.render(node, obj)
expected = """
<document>
<ns0:Address xmlns:ns0="http://tests.python-zeep.org/">
<ns0:first_name>foo-1</ns0:first_name>
<ns0:last_name>bar-1</ns0:last_name>
<ns0:first_name>foo-2</ns0:first_name>
<ns0:last_name>bar-2</ns0:last_name>
</ns0:Address>
</document>
"""
assert_nodes_equal(expected, node)
obj = address_type.parse(node[0], None)
assert obj._value_1[0]['first_name'] == 'foo-1'
assert obj._value_1[0]['last_name'] == 'bar-1'
assert obj._value_1[1]['first_name'] == 'foo-2'
assert obj._value_1[1]['last_name'] == 'bar-2'
def test_xml_multiple_groups_in_sequence():
schema = xsd.Schema(load_xml("""
<?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>
"""))
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_xml_group_methods():
schema = xsd.Schema(load_xml("""
<?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:group name="Group">
<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>
"""))
Group = schema.get_group('{http://tests.python-zeep.org/}Group')
assert Group.signature(schema) == (
'ns0:Group(city: xsd:string, country: xsd:string)')
assert str(Group) == (
'{http://tests.python-zeep.org/}Group(city: xsd:string, country: xsd:string)')
assert len(list(Group)) == 2

View File

@ -0,0 +1,477 @@
import pytest
from lxml import etree
from tests.utils import load_xml, render_node, assert_nodes_equal
from zeep import xsd
def test_build_occurs_1():
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()),
])
))
obj = custom_type(item_1='foo', item_2='bar')
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'
result = render_node(custom_type, obj)
expected = load_xml("""
<document>
<ns0:authentication xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_1>foo</ns0:item_1>
<ns0:item_2>bar</ns0:item_2>
</ns0:authentication>
</document>
""")
assert_nodes_equal(result, expected)
obj = custom_type.parse(expected[0], None)
assert obj.item_1 == 'foo'
assert obj.item_2 == 'bar'
def test_build_occurs_1_skip_value():
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()),
])
))
obj = custom_type(item_1=xsd.SkipValue, item_2='bar')
assert obj.item_1 == xsd.SkipValue
assert obj.item_2 == 'bar'
result = render_node(custom_type, obj)
expected = load_xml("""
<document>
<ns0:authentication xmlns:ns0="http://tests.python-zeep.org/">
<ns0:item_2>bar</ns0:item_2>
</ns0:authentication>
</document>
""")
assert_nodes_equal(result, expected)
def test_build_min_occurs_2_max_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)
))
assert custom_type.signature()
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 = load_xml("""
<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_build_min_occurs_2_max_occurs_2_error():
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)
))
with pytest.raises(TypeError):
custom_type(_value_1={
'item_1': 'foo-1', 'item_2': 'bar-1', 'error': True
})
def test_build_sequence_and_attributes():
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 = load_xml("""
<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_build_sequence_with_optional_elements():
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_build_max_occurs_unbounded():
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()),
], max_occurs='unbounded')
))
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._value_1 == [
{
'item_1': 'foo',
'item_2': 'bar',
}
]
def test_xml_sequence_with_choice():
schema = xsd.Schema(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/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("""
<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>
""")
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_xml_sequence_with_choice_max_occurs_2():
schema = xsd.Schema(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/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("""
<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>
""")
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_xml_sequence_with_choice_max_occurs_3():
schema = xsd.Schema(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/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("""
<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>
""")
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_xml_sequence_with_nil_element():
schema = xsd.Schema(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/">
<element name="container">
<complexType>
<sequence>
<element name="item" type="xsd:string" maxOccurs="unbounded"/>
</sequence>
</complexType>
</element>
</schema>
"""))
xml = load_xml("""
<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/">
<tns:item>text-1</tns:item>
<tns:item>text-2</tns:item>
<tns:item/>
<tns:item>text-4</tns:item>
<tns:item>text-5</tns:item>
</tns:container>
""")
elm = schema.get_element('{http://tests.python-zeep.org/}container')
result = elm.parse(xml, schema)
assert result.item == [
'text-1',
'text-2',
None,
'text-4',
'text-5',
]
def test_xml_sequence_unbounded():
schema = xsd.Schema(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="ValueListType">
<sequence maxOccurs="unbounded" minOccurs="0">
<element ref="tns:Value"/>
</sequence>
</complexType>
<element name="ValueList" type="tns:ValueListType"/>
<element name="Value" type="tns:LongName"/>
<simpleType name="LongName">
<restriction base="string">
<maxLength value="256"/>
</restriction>
</simpleType>
</schema>
"""))
elm_type = schema.get_type('{http://tests.python-zeep.org/}ValueListType')
with pytest.raises(TypeError):
elm_type(Value='bla')
elm_type(_value_1={'Value': 'bla'})
def test_xml_sequence_recover_from_missing_element():
schema = xsd.Schema(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="container">
<sequence>
<element name="item_1" type="xsd:string"/>
<element name="item_2" type="xsd:string"/>
<element name="item_3" type="xsd:string"/>
<element name="item_4" type="xsd:string"/>
</sequence>
</complexType>
</schema>
"""), strict=False)
xml = load_xml("""
<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/">
<tns:item_1>text-1</tns:item_1>
<tns:item_3>text-3</tns:item_3>
<tns:item_4>text-4</tns:item_4>
</tns:container>
""")
elm_type = schema.get_type('{http://tests.python-zeep.org/}container')
result = elm_type.parse_xmlelement(xml, schema)
assert result.item_1 == 'text-1'
assert result.item_2 is None
assert result.item_3 == 'text-3'
assert result.item_4 == 'text-4'

View File

@ -3,11 +3,11 @@ import copy
import pytest
from lxml import etree
from tests.utils import assert_nodes_equal, load_xml
from tests.utils import assert_nodes_equal, load_xml, render_node
from zeep import xsd
def test_complex_type_nested_wrong_type():
def test_xml_complex_type_nested_wrong_type():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -36,7 +36,7 @@ def test_complex_type_nested_wrong_type():
container_elm(item={'bar': 1})
def test_element_with_annotation():
def test_xml_element_with_annotation():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -61,7 +61,7 @@ def test_element_with_annotation():
address_type(foo='bar')
def test_complex_type_parsexml():
def test_xml_complex_type_parsexml():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -90,8 +90,8 @@ def test_complex_type_parsexml():
assert obj.foo == 'bar'
def test_array():
node = etree.fromstring("""
def test_xml_array():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -105,9 +105,7 @@ def test_array():
</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')
@ -130,8 +128,8 @@ def test_array():
assert_nodes_equal(expected, node)
def test_complex_type_unbounded_one():
node = etree.fromstring("""
def test_xml_complex_type_unbounded_one():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -145,9 +143,7 @@ def test_complex_type_unbounded_one():
</complexType>
</element>
</schema>
""".strip())
schema = xsd.Schema(node)
"""))
address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
obj = address_type(foo=['foo'])
@ -164,8 +160,8 @@ def test_complex_type_unbounded_one():
assert_nodes_equal(expected, node)
def test_complex_type_unbounded_named():
node = etree.fromstring("""
def test_xml_complex_type_unbounded_named():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -178,9 +174,8 @@ def test_complex_type_unbounded_named():
</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 == []
@ -201,8 +196,8 @@ def test_complex_type_unbounded_named():
assert_nodes_equal(expected, node)
def test_complex_type_array_to_other_complex_object():
node = etree.fromstring("""
def test_xml_complex_type_array_to_other_complex_object():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="Address">
@ -217,9 +212,8 @@ def test_complex_type_array_to_other_complex_object():
</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 == []
@ -227,21 +221,26 @@ def test_complex_type_array_to_other_complex_object():
obj.Address.append(schema.get_type('Address')(foo='foo'))
obj.Address.append(schema.get_type('Address')(foo='bar'))
node = etree.fromstring("""
expected = """
<?xml version="1.0"?>
<ArrayOfAddress>
<Address>
<foo>foo</foo>
</Address>
<Address>
<foo>bar</foo>
</Address>
</ArrayOfAddress>
""".strip())
<document>
<ArrayOfAddress>
<Address>
<foo>foo</foo>
</Address>
<Address>
<foo>bar</foo>
</Address>
</ArrayOfAddress>
</document>
"""
result = render_node(address_array, obj)
assert_nodes_equal(expected, result)
def test_complex_type_init_kwargs():
node = etree.fromstring("""
def test_xml_complex_type_init_kwargs():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -256,9 +255,8 @@ def test_complex_type_init_kwargs():
</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')
@ -267,8 +265,8 @@ def test_complex_type_init_kwargs():
assert obj.Email == 'j.doe@example.com'
def test_complex_type_init_args():
node = etree.fromstring("""
def test_xml_complex_type_init_args():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -283,9 +281,7 @@ def test_complex_type_init_args():
</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'
@ -293,107 +289,9 @@ def test_complex_type_init_args():
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():
def test_xml_element_ref_missing_namespace():
# For buggy soap servers (#170)
node = etree.fromstring("""
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -407,9 +305,7 @@ def test_element_ref_missing_namespace():
</complexType>
</element>
</schema>
""".strip())
schema = xsd.Schema(node)
"""))
custom_type = schema.get_element('{http://tests.python-zeep.org/}bar')
input_xml = load_xml("""
@ -421,8 +317,8 @@ def test_element_ref_missing_namespace():
assert item.foo == 'bar'
def test_element_ref():
node = etree.fromstring("""
def test_xml_element_ref():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -437,9 +333,7 @@ def test_element_ref():
</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)
@ -460,8 +354,8 @@ def test_element_ref():
assert_nodes_equal(expected, node)
def test_element_ref_occurs():
node = etree.fromstring("""
def test_xml_element_ref_occurs():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -477,9 +371,7 @@ def test_element_ref_occurs():
</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)
@ -500,8 +392,8 @@ def test_element_ref_occurs():
assert_nodes_equal(expected, node)
def test_unqualified():
node = etree.fromstring("""
def test_xml_unqualified():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -517,9 +409,8 @@ def test_unqualified():
</complexType>
</element>
</schema>
""".strip())
"""))
schema = xsd.Schema(node)
address_type = schema.get_element('{http://tests.python-zeep.org/}Address')
obj = address_type(foo='bar')
@ -536,8 +427,8 @@ def test_unqualified():
assert_nodes_equal(expected, node)
def test_defaults():
node = etree.fromstring("""
def test_xml_defaults():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -547,25 +438,40 @@ def test_defaults():
<xsd:element name="container">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="foo" type="xsd:string" default="hoi"/>
<xsd:element name="item_1" type="xsd:string" default="hoi"/>
<xsd:element name="item_2" type="xsd:string" default="hoi" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="bar" type="xsd:string" default="hoi"/>
<xsd:attribute name="attr_1" 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"
assert obj.item_1 == "hoi"
assert obj.item_2 is None
assert obj.attr_1 == "hoi"
expected = """
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/" bar="hoi">
<ns0:foo>hoi</ns0:foo>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/" attr_1="hoi">
<ns0:item_1>hoi</ns0:item_1>
</ns0:container>
</document>
"""
node = etree.Element('document')
container_type.render(node, obj)
assert_nodes_equal(expected, node)
obj.item_2 = 'ok'
expected = """
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/" attr_1="hoi">
<ns0:item_1>hoi</ns0:item_1>
<ns0:item_2>ok</ns0:item_2>
</ns0:container>
</document>
"""
@ -574,8 +480,8 @@ def test_defaults():
assert_nodes_equal(expected, node)
def test_defaults_parse():
node = etree.fromstring("""
def test_xml_defaults_parse_boolean():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -585,29 +491,65 @@ def test_defaults_parse():
<xsd:element name="container">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="foo" type="xsd:string" default="hoi" minOccurs="0"/>
<xsd:element name="foo" type="xsd:boolean" default="false"/>
</xsd:sequence>
<xsd:attribute name="bar" type="xsd:string" default="hoi"/>
<xsd:attribute name="bar" type="xsd:boolean" default="0"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
""".strip())
"""))
container_type = schema.get_element(
'{http://tests.python-zeep.org/}container')
obj = container_type()
assert obj.foo == "false"
assert obj.bar == "0"
expected = """
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/" bar="false">
<ns0:foo>false</ns0:foo>
</ns0:container>
</document>
"""
node = etree.Element('document')
container_type.render(node, obj)
assert_nodes_equal(expected, node)
def test_xml_defaults_parse():
schema = xsd.Schema(load_xml("""
<?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="item_1" type="xsd:string" default="hoi" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="attr_1" type="xsd:string" default="hoi"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""))
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:item_1>hoi</ns0:item_1>
</ns0:container>
""")
item = container_elm.parse(node, schema)
assert item.bar == 'hoi'
assert item.attr_1 == 'hoi'
def test_init_with_dicts():
node = etree.fromstring("""
def test_xml_init_with_dicts():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -638,9 +580,8 @@ def test_init_with_dicts():
</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'}]})
@ -662,9 +603,8 @@ def test_init_with_dicts():
assert_nodes_equal(expected, node)
def test_sequence_in_sequence():
node = load_xml("""
def test_xml_sequence_in_sequence():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema
xmlns="http://www.w3.org/2001/XMLSchema"
@ -684,8 +624,7 @@ def test_sequence_in_sequence():
</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")
@ -703,7 +642,7 @@ def test_sequence_in_sequence():
assert_nodes_equal(expected, node)
def test_sequence_in_sequence_many():
def test_xml_sequence_in_sequence_many():
node = load_xml("""
<?xml version="1.0"?>
<schema
@ -753,8 +692,8 @@ def test_sequence_in_sequence_many():
assert_nodes_equal(expected, node)
def test_complex_type_empty():
node = etree.fromstring("""
def test_xml_complex_type_empty():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -769,9 +708,7 @@ def test_complex_type_empty():
</complexType>
</element>
</schema>
""".strip())
schema = xsd.Schema(node)
"""))
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
obj = container_elm(something={})
@ -790,7 +727,7 @@ def test_complex_type_empty():
assert item.something is None
def test_schema_as_payload():
def test_xml_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/"
@ -834,7 +771,7 @@ def test_schema_as_payload():
assert value._value_1['item-2'] == 'value-2'
def test_nill():
def test_xml_nill():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
@ -868,8 +805,8 @@ def test_nill():
assert_nodes_equal(expected, node)
def test_empty_xmlns():
node = load_xml("""
def test_xml_empty_xmlns():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
@ -885,9 +822,7 @@ def test_empty_xmlns():
</complexType>
</element>
</schema>
""".strip())
schema = xsd.Schema(node)
"""))
container_elm = schema.get_element('{http://tests.python-zeep.org/}container')
node = load_xml("""
@ -905,8 +840,8 @@ def test_empty_xmlns():
assert item._value_1 == 'foo'
def test_keep_objects_intact():
node = etree.fromstring("""
def test_xml_keep_objects_intact():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xsd:schema
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
@ -937,9 +872,8 @@ def test_keep_objects_intact():
</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'}]})

View File

@ -7,132 +7,6 @@ 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_max_occurs_infinite_loop():
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()),
], max_occurs='unbounded')
))
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._value_1 == [
{
'item_1': 'foo',
'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"?>
@ -239,7 +113,7 @@ def test_sequence_parse_anytype_obj():
'{http://www.w3.org/2001/XMLSchema}Schema',
targetNamespace='http://tests.python-zeep.org/'))
root = next(schema.documents)
root = schema.root_document
root.register_type('{http://tests.python-zeep.org/}something', value_type)
custom_type = xsd.Element(
@ -264,147 +138,6 @@ def test_sequence_parse_anytype_obj():
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"?>
@ -465,177 +198,6 @@ def test_sequence_parse_anytype_regression_17():
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(
@ -723,7 +285,7 @@ def test_nested_complex_type_optional():
""")
obj = custom_type.parse(expected, None)
assert obj.item_1 == 'foo'
assert obj.item_2 == []
assert obj.item_2 == [None]
expected = etree.fromstring("""
<ns0:container xmlns:ns0="http://tests.python-zeep.org/">
@ -885,3 +447,19 @@ def test_parse_invalid_values():
assert result.item_2 == datetime.date(2016, 10, 20)
assert result.attr_1 is None
assert result.attr_2 == datetime.date(2013, 10, 20)
def test_xsd_missing_localname():
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" type="xsd:"/>
</schema>
"""))
schema.get_element('{http://tests.python-zeep.org/}container')

View File

@ -4,6 +4,8 @@ from lxml import etree
from tests.utils import DummyTransport, load_xml
from zeep import exceptions, xsd
from zeep.xsd import Schema
from zeep.xsd.types.unresolved import UnresolvedType
from tests.utils import assert_nodes_equal, load_xml, render_node
def test_default_types():
@ -98,7 +100,7 @@ def test_invalid_localname_handling():
def test_schema_repr_none():
schema = xsd.Schema()
assert repr(schema) == "<Schema(location='<none>')>"
assert repr(schema) == "<Schema()>"
def test_schema_repr_val():
@ -111,7 +113,7 @@ def test_schema_repr_val():
elementFormDefault="qualified">
</xs:schema>
"""))
assert repr(schema) == "<Schema(location=None)>"
assert repr(schema) == "<Schema(location=None, tns='http://tests.python-zeep.org/')>"
def test_schema_doc_repr_val():
@ -433,8 +435,8 @@ def test_duplicate_target_namespace():
elm_b = schema.get_element('{http://tests.python-zeep.org/duplicate}elm-in-b')
elm_c = schema.get_element('{http://tests.python-zeep.org/duplicate}elm-in-c')
assert not isinstance(elm_b.type, xsd.UnresolvedType)
assert not isinstance(elm_c.type, xsd.UnresolvedType)
assert not isinstance(elm_b.type, UnresolvedType)
assert not isinstance(elm_c.type, UnresolvedType)
def test_multiple_no_namespace():
@ -644,6 +646,136 @@ def test_include_recursion():
schema.get_element('{http://tests.python-zeep.org/b}bar')
def test_include_relative():
node_a = etree.fromstring("""
<?xml version="1.0"?>
<xs:schema
xmlns="http://tests.python-zeep.org/tns"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://tests.python-zeep.org/a"
elementFormDefault="qualified">
<xs:include schemaLocation="http://tests.python-zeep.org/subdir/b.xsd"/>
</xs:schema>
""".strip())
node_b = etree.fromstring("""
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:include schemaLocation="c.xsd"/>
<xs:element name="bar" type="xs:string"/>
</xs:schema>
""".strip())
node_c = etree.fromstring("""
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="foo" type="xs:string"/>
</xs:schema>
""".strip())
transport = DummyTransport()
transport.bind('http://tests.python-zeep.org/subdir/b.xsd', node_b)
transport.bind('http://tests.python-zeep.org/subdir/c.xsd', node_c)
schema = xsd.Schema(node_a, transport=transport)
schema.get_element('{http://tests.python-zeep.org/a}foo')
schema.get_element('{http://tests.python-zeep.org/a}bar')
def test_include_no_default_namespace():
node_a = etree.fromstring("""
<?xml version="1.0"?>
<xs:schema
xmlns="http://tests.python-zeep.org/tns"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://tests.python-zeep.org/tns"
elementFormDefault="qualified">
<xs:include
schemaLocation="http://tests.python-zeep.org/b.xsd"/>
<xs:element name="container" type="foo"/>
</xs:schema>
""".strip())
# include without default namespace, other xsd prefix
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">
<xsd:simpleType name="my-string">
<xsd:restriction base="xsd:boolean"/>
</xsd:simpleType>
<xsd:complexType name="foo">
<xsd:sequence>
<xsd:element name="item" type="my-string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
""".strip())
transport = DummyTransport()
transport.bind('http://tests.python-zeep.org/b.xsd', node_b)
schema = xsd.Schema(node_a, transport=transport)
item = schema.get_element('{http://tests.python-zeep.org/tns}container')
assert item
def test_include_different_form_defaults():
node_a = etree.fromstring("""
<?xml version="1.0"?>
<xs:schema
xmlns="http://tests.python-zeep.org/"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://tests.python-zeep.org/">
<xs:include
schemaLocation="http://tests.python-zeep.org/b.xsd"/>
</xs:schema>
""".strip())
# include without default namespace, other xsd prefix
node_b = load_xml("""
<?xml version="1.0"?>
<xsd:schema
elementFormDefault="qualified"
attributeFormDefault="qualified"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/b">
<xsd:element name="container" type="foo"/>
<xsd:complexType name="foo">
<xsd:sequence>
<xsd:element name="item" type="xsd:string"/>
</xsd:sequence>
<xsd:attribute name="attr" type="xsd:string"/>
</xsd:complexType>
</xsd:schema>
""")
transport = DummyTransport()
transport.bind('http://tests.python-zeep.org/b.xsd', node_b)
schema = xsd.Schema(node_a, transport=transport)
item = schema.get_element('{http://tests.python-zeep.org/}container')
obj = item(item='foo', attr='bar')
node = render_node(item, obj)
expected = load_xml("""
<document>
<ns0:container xmlns:ns0="http://tests.python-zeep.org/" ns0:attr="bar">
<ns0:item>foo</ns0:item>
</ns0:container>
</document>
""")
assert_nodes_equal(expected, node)
def test_merge():
node_a = etree.fromstring("""
<?xml version="1.0"?>
@ -674,3 +806,37 @@ def test_merge():
schema_a.get_element('{http://tests.python-zeep.org/a}foo')
schema_a.get_element('{http://tests.python-zeep.org/b}foo')
def test_xml_namespace():
xmlns = load_xml("""
<?xml version="1.0"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xml="http://www.w3.org/XML/1998/namespace"
targetNamespace="http://www.w3.org/XML/1998/namespace"
elementFormDefault="qualified">
<xs:attribute name="lang" type="xs:string"/>
</xs:schema>
""")
transport = DummyTransport()
transport.bind('http://www.w3.org/2001/xml.xsd', xmlns)
xsd.Schema(load_xml("""
<?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:import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2001/xml.xsd"/>
<xs:element name="container">
<xs:complexType>
<xs:sequence/>
<xs:attribute ref="xml:lang"/>
</xs:complexType>
</xs:element>
</xs:schema>
"""), transport=transport)

View File

@ -1,6 +1,7 @@
from lxml import etree
from zeep import xsd
from tests.utils import load_xml
def test_signature_complex_type_choice():
@ -16,7 +17,7 @@ def test_signature_complex_type_choice():
xsd.String()),
])
))
assert custom_type.signature() == '({item_1: xsd:string} | {item_2: xsd:string})'
assert custom_type.signature() == '{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {item_2: xsd:string}))'
def test_signature_complex_type_choice_sequence():
@ -38,7 +39,7 @@ def test_signature_complex_type_choice_sequence():
])
))
assert custom_type.signature() == (
'({item_1: xsd:string} | {item_2_1: xsd:string, item_2_2: xsd:string})')
'{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {item_2_1: xsd:string, item_2_2: xsd:string}))')
def test_signature_nested_sequences():
@ -80,7 +81,7 @@ def test_signature_nested_sequences():
))
assert custom_type.signature() == (
'item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string})' # noqa
'{http://tests.python-zeep.org/}authentication(item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string}))'
)
@ -123,7 +124,7 @@ def test_signature_nested_sequences_multiple():
))
assert custom_type.signature() == (
'item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, _value_1: ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string})[]' # noqa
'{http://tests.python-zeep.org/}authentication(item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string})[])'
)
@ -138,7 +139,7 @@ def test_signature_complex_type_any():
xsd.Any()
])
))
assert custom_type.signature() == '({item_1: xsd:string} | {_value_1: ANY})'
assert custom_type.signature() == '{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {_value_1: ANY}))'
custom_type(item_1='foo')
@ -161,7 +162,7 @@ def test_signature_complex_type_sequence_with_any():
])
))
assert custom_type.signature() == (
'({item_1: xsd:string} | {item_2: {_value_1: ANY}})')
'{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {item_2: {_value_1: ANY}}))')
def test_signature_complex_type_sequence_with_anys():
@ -184,4 +185,30 @@ def test_signature_complex_type_sequence_with_anys():
])
))
assert custom_type.signature() == (
'({item_1: xsd:string} | {item_2: {_value_1: ANY, _value_2: ANY}})')
'{http://tests.python-zeep.org/}authentication(' +
'({item_1: xsd:string} | {item_2: {_value_1: ANY, _value_2: ANY}})' +
')')
def test_schema_recursive_ref():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<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="tns:Container" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""))
elm = schema.get_element('ns0:Container')
elm.signature(schema)

View File

@ -6,6 +6,7 @@ def test_union_same_types():
schema = xsd.Schema(load_xml("""
<?xml version="1.0"?>
<xsd:schema
xmlns="http://tests.python-zeep.org/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/"
targetNamespace="http://tests.python-zeep.org/"
@ -21,7 +22,7 @@ def test_union_same_types():
</xsd:simpleType>
<xsd:simpleType name="Date">
<xsd:union memberTypes="MMYY MMYYYY"/>
<xsd:union memberTypes="tns:MMYY MMYYYY"/>
</xsd:simpleType>
<xsd:element name="item" type="tns:Date"/>
</xsd:schema>
@ -49,7 +50,7 @@ def test_union_mixed():
elementFormDefault="qualified">
<xsd:element name="item" type="tns:Date"/>
<xsd:simpleType name="Date">
<xsd:union memberTypes="xsd:date xsd:gYear xsd:gYearMonth MMYY MMYYYY"/>
<xsd:union memberTypes="xsd:date xsd:gYear xsd:gYearMonth tns:MMYY tns:MMYYYY"/>
</xsd:simpleType>
<xsd:simpleType name="MMYY">
<xsd:restriction base="xsd:string">

View File

@ -1,5 +1,9 @@
import json
import pickle
import pytest
import six
from lxml.etree import QName
from zeep import xsd
from zeep.xsd import valueobjects
@ -214,9 +218,10 @@ def test_choice_mixed():
xsd.Element('item_2', xsd.String()),
]),
xsd.Element('item_2', xsd.String())
])
]),
qname=QName('http://tests.python-zeep.org', 'container')
)
expected = '({item_1: xsd:string} | {item_2: xsd:string}), item_2__1: xsd:string'
expected = '{http://tests.python-zeep.org}container(({item_1: xsd:string} | {item_2: xsd:string}), item_2__1: xsd:string)'
assert xsd_type.signature() == expected
args = tuple([])
@ -401,3 +406,35 @@ def test_choice_sequences_init_dict():
{'item_1': 'value-1', 'item_2': 'value-2'}
]
}
def test_pickle():
xsd_type = xsd.ComplexType(
xsd.Sequence([
xsd.Element('item_1', xsd.String()),
xsd.Element('item_2', xsd.String())
]))
obj = xsd_type(item_1='x', item_2='y')
data = pickle.dumps(obj)
obj_rt = pickle.loads(data)
assert obj.item_1 == 'x'
assert obj.item_2 == 'y'
assert obj_rt.item_1 == 'x'
assert obj_rt.item_2 == 'y'
def test_json():
xsd_type = xsd.ComplexType(
xsd.Sequence([
xsd.Element('item_1', xsd.String()),
xsd.Element('item_2', xsd.String())
]))
obj = xsd_type(item_1='x', item_2='y')
assert obj.__json__() == {
'item_1': 'x',
'item_2': 'y',
}

View File

@ -23,7 +23,7 @@ def test_schema_empty():
</schema>
""")
schema = parse_schema_node(node)
root = next(schema.documents)
root = schema._get_schema_documents('http://tests.python-zeep.org/')[0]
assert root._element_form == 'qualified'
assert root._attribute_form == 'unqualified'

View File

@ -1,13 +1,13 @@
<?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/"
<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"
name="StockQuote"
targetNamespace="http://example.com/stockquote.wsdl">
<types>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/stockquote.xsd"
xmlns:tns="http://example.com/stockquote.xsd" >
<complexType name="Address">
@ -17,6 +17,12 @@
<element minOccurs="0" maxOccurs="1" name="Email" type="string"/>
</sequence>
</complexType>
<complexType name="ArrayOfAddress">
<sequence>
<element name="Address" type="tns:Address" minOccurs="0" maxOccurs="unbounded"/>
</sequence>
</complexType>
<element name="Fault1">
<complexType>
<sequence>
@ -87,6 +93,9 @@
<fault message="tns:FaultMessageMsg1" name="fault1"/>
<fault message="tns:FaultMessageMsg2" name="fault2"/>
</operation>
<operation name="GetLastTradePriceNoOutput">
<input message="tns:GetLastTradePriceInput"/>
</operation>
</portType>
<binding name="StockQuoteBinding" type="tns:StockQuotePortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
@ -105,6 +114,12 @@
<soap:fault name="fault2" use="literal"/>
</fault>
</operation>
<operation name="GetLastTradePriceNoOutput">
<soap:operation soapAction="http://example.com/GetLastTradePrice"/>
<input>
<soap:body use="literal"/>
</input>
</operation>
</binding>
<service name="StockQuoteService">
<documentation>My first service</documentation>

View File

@ -1,7 +1,16 @@
<?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">
<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/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
name="StockQuote"
targetNamespace="http://example.com/stockquote.wsdl">
<types>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/stockquote.xsd">
<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"/>
@ -48,6 +57,10 @@
<message name="GetLastTradePriceOutput">
<part name="body" element="xsd1:TradePrice"/>
</message>
<message name="OptionalResponseHeader">
<part name="body" type="xsd:string"/>
</message>
<portType name="StockQuotePortType">
<operation name="GetLastTradePrice">
<input message="tns:GetLastTradePriceInput"/>
@ -65,6 +78,7 @@
</input>
<output>
<soap:body use="literal"/>
<soap:header message="tns:OptionalResponseHeader" part="body" use="literal"/>
</output>
</operation>
</binding>