From 9ee5ee5a7cf1ee09a4c94a36001dff5fdb1424f3 Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Mon, 4 Sep 2017 16:23:14 +0200 Subject: [PATCH] Merging upstream version 2.4.0. --- CHANGES | 26 +++ CONTRIBUTORS.rst | 41 +++-- PKG-INFO | 6 +- README.rst | 4 +- setup.cfg | 2 +- setup.py | 17 +- src/zeep.egg-info/PKG-INFO | 6 +- src/zeep.egg-info/SOURCES.txt | 6 + src/zeep.egg-info/requires.txt | 11 +- src/zeep/__init__.py | 2 +- src/zeep/asyncio/transport.py | 15 +- src/zeep/client.py | 12 +- src/zeep/exceptions.py | 9 +- src/zeep/loader.py | 5 +- src/zeep/tornado/__init__.py | 2 + src/zeep/tornado/bindings.py | 28 +++ src/zeep/tornado/transport.py | 133 ++++++++++++++ src/zeep/transports.py | 1 - src/zeep/utils.py | 3 +- src/zeep/wsdl/attachments.py | 2 +- src/zeep/wsdl/bindings/soap.py | 19 +- src/zeep/wsdl/messages/multiref.py | 143 +++++++++++---- src/zeep/wsdl/messages/soap.py | 22 +-- src/zeep/wsdl/messages/xop.py | 26 +++ src/zeep/wsdl/parse.py | 8 +- src/zeep/wsdl/utils.py | 2 +- src/zeep/wsdl/wsdl.py | 9 +- src/zeep/wsse/signature.py | 31 ++-- src/zeep/xsd/const.py | 6 + src/zeep/xsd/elements/any.py | 2 +- src/zeep/xsd/elements/indicators.py | 4 +- src/zeep/xsd/schema.py | 19 +- src/zeep/xsd/types/builtins.py | 10 +- src/zeep/xsd/types/complex.py | 9 +- src/zeep/xsd/visitor.py | 8 +- tests/test_asyncio_transport.py | 18 +- tests/test_client.py | 14 ++ tests/test_soap_multiref.py | 137 ++++++++++++++- tests/test_soap_xop.py | 253 +++++++++++++++++++++++++++ tests/test_tornado_transport.py | 57 ++++++ tests/test_wsdl_messages_document.py | 65 +++++++ tests/test_wsdl_soap.py | 31 +++- tests/test_wsse_signature.py | 3 +- tests/test_xsd_builtins.py | 1 + tests/test_xsd_indicators_group.py | 36 ++++ 45 files changed, 1133 insertions(+), 131 deletions(-) create mode 100644 src/zeep/tornado/__init__.py create mode 100644 src/zeep/tornado/bindings.py create mode 100644 src/zeep/tornado/transport.py create mode 100644 src/zeep/wsdl/messages/xop.py create mode 100644 tests/test_soap_xop.py create mode 100644 tests/test_tornado_transport.py diff --git a/CHANGES b/CHANGES index 8bbebc5..55771b8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,29 @@ +2.4.0 (2017-08-26) +------------------ + - Add support for tornado async transport via gen.coroutine (#530, Kateryna Burda) + - Check if soap:address is defined in the service port instead of raising an + exception (#527) + - Update packaging (stop using find_packages()) (#529) + - Properly handle None values when rendering complex types (#526) + - Fix generating signature for empty wsdl messages (#542) + - Support passing strings to xsd:Time objects (#540) + + +2.3.0 (2017-08-06) +------------------ + - The XML send to the server is no longer using ``pretty_print=True`` (#484) + - Refactor of the multiref support to fix issues with child elements (#489) + - Add workaround to support negative durations (#486) + - Fix creating XML documents for operations without aguments (#479) + - Fix xsd:extension on xsd:group elements (#523) + + +2.2.0 (2017-06-19) +------------------ + - Automatically import the soap-encoding schema if it is required (#473) + - Add support for XOP messages (this is a rewrite of #325 by vashek) + + 2.1.1 (2017-06-11) ------------------ - Fix previous release, it contained an incorrect dependency (Mock 2.1.) due diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 44a9546..063f6ce 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -4,42 +4,51 @@ Authors Contributors ============ -* vashek + +* Kateryna Burda +* Alexey Stepanov * Marco Vellinga * jaceksnet * Andrew Serong -* Joeri Bekker -* Eric Wong -* Jacek Stępniewski -* Alexey Stepanov +* vashek +* Seppo Yli-Olli +* Sam Denton +* Dani Möller * Julien Delasoie +* Christian González * bjarnagin * mcordes -* Sam Denton -* David Baumgold +* Joeri Bekker +* Bartek Wójcicki +* jhorman * fiebiga +* David Baumgold * Antonio Cuni * Alexandre de Mari -* Jason Vertrees * Nicolas Evrard +* Eric Wong +* Jason Vertrees +* Falldog * Matt Grimm (mgrimm) * Marek Wywiał -* Falldog * btmanm * Caleb Salt -* Julien Marechal -* Mike Fiedler -* Dave Wapstra -* OrangGeeGee -* Stefano Parmesan +* Ondřej Lanč * Jan Murre -* Ben Tucker +* Stefano Parmesan +* Julien Marechal +* Dave Wapstra +* Mike Fiedler +* Derek Harland * Bruno Duyé * Christoph Heuel -* Derek Harland +* Ben Tucker * Eric Waller * Falk Schuetzenmeister * Jon Jenkins +* OrangGeeGee * Raymond Piller * Zoltan Benedek * Øyvind Heddeland Instefjord + + diff --git a/PKG-INFO b/PKG-INFO index 6f21657..2d57228 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: zeep -Version: 2.1.1 +Version: 2.4.0 Summary: A modern/fast Python SOAP client based on lxml / requests Home-page: http://docs.python-zeep.org Author: Michael van Tellingen @@ -18,7 +18,9 @@ Description: ======================== * Support for Soap 1.1, Soap 1.2 and HTTP bindings * Support for WS-Addressing headers * Support for WSSE (UserNameToken / x.509 signing) - * Experimental support for asyncio via aiohttp (Python 3.5+) + * Support for tornado async transport via gen.coroutine (Python 2.7+) + * Support for asyncio via aiohttp (Python 3.5+) + * Experimental support for XOP messages Please see for more information the documentation at diff --git a/README.rst b/README.rst index 647b78c..fde244e 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,9 @@ Highlights: * Support for Soap 1.1, Soap 1.2 and HTTP bindings * Support for WS-Addressing headers * Support for WSSE (UserNameToken / x.509 signing) - * Experimental support for asyncio via aiohttp (Python 3.5+) + * Support for tornado async transport via gen.coroutine (Python 2.7+) + * Support for asyncio via aiohttp (Python 3.5+) + * Experimental support for XOP messages Please see for more information the documentation at diff --git a/setup.cfg b/setup.cfg index e77a765..54d59b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.1.1 +current_version = 2.4.0 commit = true tag = true tag_name = {new_version} diff --git a/setup.py b/setup.py index dff575e..edd6a14 100755 --- a/setup.py +++ b/setup.py @@ -19,6 +19,10 @@ docs_require = [ 'sphinx>=1.4.0', ] +tornado_require = [ + 'tornado>=4.0.2' +] + async_require = [] # see below xmlsec_require = [ @@ -29,15 +33,17 @@ tests_require = [ 'freezegun==0.3.8', 'mock==2.0.0', 'pretend==1.0.8', - 'pytest-cov==2.4.0', - 'pytest==3.0.6', + 'pytest-cov==2.5.1', + 'pytest==3.1.3', 'requests_mock>=0.7.0', + 'pytest-tornado==0.4.5', # Linting 'isort==4.2.5', - 'flake8==3.2.1', + 'flake8==3.3.0', 'flake8-blind-except==0.1.1', 'flake8-debugger==1.4.0', + 'flake8-imports==0.1.1', ] @@ -52,7 +58,7 @@ with open('README.rst') as fh: setup( name='zeep', - version='2.1.1', + version='2.4.0', description='A modern/fast Python SOAP client based on lxml / requests', long_description=long_description, author="Michael van Tellingen", @@ -65,11 +71,12 @@ setup( 'docs': docs_require, 'test': tests_require, 'async': async_require, + 'tornado': tornado_require, 'xmlsec': xmlsec_require, }, entry_points={}, package_dir={'': 'src'}, - packages=find_packages('src'), + packages=['zeep'], include_package_data=True, license='MIT', diff --git a/src/zeep.egg-info/PKG-INFO b/src/zeep.egg-info/PKG-INFO index 6f21657..2d57228 100644 --- a/src/zeep.egg-info/PKG-INFO +++ b/src/zeep.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: zeep -Version: 2.1.1 +Version: 2.4.0 Summary: A modern/fast Python SOAP client based on lxml / requests Home-page: http://docs.python-zeep.org Author: Michael van Tellingen @@ -18,7 +18,9 @@ Description: ======================== * Support for Soap 1.1, Soap 1.2 and HTTP bindings * Support for WS-Addressing headers * Support for WSSE (UserNameToken / x.509 signing) - * Experimental support for asyncio via aiohttp (Python 3.5+) + * Support for tornado async transport via gen.coroutine (Python 2.7+) + * Support for asyncio via aiohttp (Python 3.5+) + * Experimental support for XOP messages Please see for more information the documentation at diff --git a/src/zeep.egg-info/SOURCES.txt b/src/zeep.egg-info/SOURCES.txt index ab2e621..df1bde9 100644 --- a/src/zeep.egg-info/SOURCES.txt +++ b/src/zeep.egg-info/SOURCES.txt @@ -34,6 +34,9 @@ src/zeep.egg-info/top_level.txt src/zeep/asyncio/__init__.py src/zeep/asyncio/bindings.py src/zeep/asyncio/transport.py +src/zeep/tornado/__init__.py +src/zeep/tornado/bindings.py +src/zeep/tornado/transport.py src/zeep/wsdl/__init__.py src/zeep/wsdl/attachments.py src/zeep/wsdl/definitions.py @@ -49,6 +52,7 @@ 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/wsdl/messages/xop.py src/zeep/wsse/__init__.py src/zeep/wsse/compose.py src/zeep/wsse/signature.py @@ -92,6 +96,8 @@ tests/test_main.py tests/test_pprint.py tests/test_response.py tests/test_soap_multiref.py +tests/test_soap_xop.py +tests/test_tornado_transport.py tests/test_transports.py tests/test_wsa.py tests/test_wsdl.py diff --git a/src/zeep.egg-info/requires.txt b/src/zeep.egg-info/requires.txt index bbcba39..3c7acf2 100644 --- a/src/zeep.egg-info/requires.txt +++ b/src/zeep.egg-info/requires.txt @@ -18,14 +18,19 @@ sphinx>=1.4.0 freezegun==0.3.8 mock==2.0.0 pretend==1.0.8 -pytest-cov==2.4.0 -pytest==3.0.6 +pytest-cov==2.5.1 +pytest==3.1.3 requests_mock>=0.7.0 +pytest-tornado==0.4.5 isort==4.2.5 -flake8==3.2.1 +flake8==3.3.0 flake8-blind-except==0.1.1 flake8-debugger==1.4.0 +flake8-imports==0.1.1 aioresponses>=0.1.3 +[tornado] +tornado>=4.0.2 + [xmlsec] xmlsec>=0.6.1 diff --git a/src/zeep/__init__.py b/src/zeep/__init__.py index faa5a67..a2c1f81 100644 --- a/src/zeep/__init__.py +++ b/src/zeep/__init__.py @@ -3,4 +3,4 @@ from zeep.transports import Transport # noqa from zeep.plugins import Plugin # noqa from zeep.xsd.valueobjects import AnyObject # noqa -__version__ = '2.1.1' +__version__ = '2.4.0' diff --git a/src/zeep/asyncio/transport.py b/src/zeep/asyncio/transport.py index ec3cc40..217d6ad 100644 --- a/src/zeep/asyncio/transport.py +++ b/src/zeep/asyncio/transport.py @@ -4,10 +4,12 @@ Adds asyncio support to Zeep. Contains Python 3.5+ only syntax! """ import asyncio import logging +from . import bindings import aiohttp from requests import Response +from zeep.exceptions import TransportError from zeep.transports import Transport from zeep.utils import get_version from zeep.wsdl.utils import etree_to_string @@ -17,7 +19,10 @@ __all__ = ['AsyncTransport'] class AsyncTransport(Transport): """Asynchronous Transport class using aiohttp.""" - supports_async = True + binding_classes = [ + bindings.AsyncSoap11Binding, + bindings.AsyncSoap12Binding, + ] def __init__(self, loop, cache=None, timeout=300, operation_timeout=None, session=None): @@ -45,6 +50,14 @@ class AsyncTransport(Transport): with aiohttp.Timeout(self.load_timeout): response = await self.session.get(url) result = await response.read() + try: + response.raise_for_status() + except aiohttp.ClientError as exc: + raise TransportError( + message=str(exc), + status_code=response.status, + content=result + ).with_traceback(exc.__traceback__) from exc # Block until we have the data self.loop.run_until_complete(_load_remote_data_async()) diff --git a/src/zeep/client.py b/src/zeep/client.py index cad16ac..d8933a9 100644 --- a/src/zeep/client.py +++ b/src/zeep/client.py @@ -167,11 +167,12 @@ class Client(object): """ - # Store current options - old_raw_raw_response = self.raw_response + if raw_response is not NotSet: + # Store current options + old_raw_response = self.raw_response - # Set new options - self.raw_response = raw_response + # Set new options + self.raw_response = raw_response if timeout is not NotSet: timeout_ctx = self.transport._options(timeout=timeout) @@ -179,7 +180,8 @@ class Client(object): yield - self.raw_response = old_raw_raw_response + if raw_response is not NotSet: + self.raw_response = old_raw_response if timeout is not NotSet: timeout_ctx.__exit__(None, None, None) diff --git a/src/zeep/exceptions.py b/src/zeep/exceptions.py index a7a4c1d..7be0773 100644 --- a/src/zeep/exceptions.py +++ b/src/zeep/exceptions.py @@ -8,7 +8,9 @@ class Error(Exception): class XMLSyntaxError(Error): - pass + def __init__(self, *args, **kwargs): + self.content = kwargs.pop('content', None) + super(XMLSyntaxError, self).__init__(*args, **kwargs) class XMLParseError(Error): @@ -35,7 +37,10 @@ class WsdlSyntaxError(Error): class TransportError(Error): - pass + def __init__(self, message='', status_code=0, content=None): + super(TransportError, self).__init__(message) + self.status_code = status_code + self.content = content class LookupError(Error): diff --git a/src/zeep/loader.py b/src/zeep/loader.py index 58ea81e..561b800 100644 --- a/src/zeep/loader.py +++ b/src/zeep/loader.py @@ -46,7 +46,10 @@ def parse_xml(content, transport, base_url=None, strict=True, try: return fromstring(content, parser=parser, base_url=base_url) except etree.XMLSyntaxError as exc: - raise XMLSyntaxError("Invalid XML content received (%s)" % exc.msg) + raise XMLSyntaxError( + "Invalid XML content received (%s)" % exc.msg, + content=content + ) def load_external(url, transport, base_url=None, strict=True): diff --git a/src/zeep/tornado/__init__.py b/src/zeep/tornado/__init__.py new file mode 100644 index 0000000..3011239 --- /dev/null +++ b/src/zeep/tornado/__init__.py @@ -0,0 +1,2 @@ +from .transport import * # noqa +from .bindings import * # noqa diff --git a/src/zeep/tornado/bindings.py b/src/zeep/tornado/bindings.py new file mode 100644 index 0000000..ba73800 --- /dev/null +++ b/src/zeep/tornado/bindings.py @@ -0,0 +1,28 @@ +from zeep.wsdl import bindings +from tornado import gen + +__all__ = ['AsyncSoap11Binding', 'AsyncSoap12Binding'] + + +class AsyncSoapBinding(object): + + @gen.coroutine + def send(self, client, options, operation, args, kwargs): + envelope, http_headers = self._create( + operation, args, kwargs, + client=client, + options=options) + + response = yield client.transport.post_xml( + options['address'], envelope, http_headers) + + operation_obj = self.get(operation) + raise gen.Return(self.process_reply(client, operation_obj, response)) + + +class AsyncSoap11Binding(AsyncSoapBinding, bindings.Soap11Binding): + pass + + +class AsyncSoap12Binding(AsyncSoapBinding, bindings.Soap12Binding): + pass diff --git a/src/zeep/tornado/transport.py b/src/zeep/tornado/transport.py new file mode 100644 index 0000000..c80d971 --- /dev/null +++ b/src/zeep/tornado/transport.py @@ -0,0 +1,133 @@ +""" +Adds async tornado.gen support to Zeep. + +""" +import logging +import urllib +from . import bindings + +from tornado import gen, httpclient +from requests import Response, Session +from requests.auth import HTTPBasicAuth, HTTPDigestAuth + +from zeep.transports import Transport +from zeep.utils import get_version +from zeep.wsdl.utils import etree_to_string + +__all__ = ['TornadoAsyncTransport'] + + +class TornadoAsyncTransport(Transport): + """Asynchronous Transport class using tornado gen.""" + binding_classes = [ + bindings.AsyncSoap11Binding, + bindings.AsyncSoap12Binding] + + def __init__(self, cache=None, timeout=300, operation_timeout=None, + session=None): + self.cache = cache + self.load_timeout = timeout + self.operation_timeout = operation_timeout + self.logger = logging.getLogger(__name__) + + self.session = session or Session() + self.session.headers['User-Agent'] = ( + 'Zeep/%s (www.python-zeep.org)' % (get_version())) + + def _load_remote_data(self, url): + client = httpclient.HTTPClient() + kwargs = { + 'method': 'GET', + 'request_timeout': self.load_timeout + } + http_req = httpclient.HTTPRequest(url, **kwargs) + response = client.fetch(http_req) + return response.body + + @gen.coroutine + def post(self, address, message, headers): + response = yield self.fetch(address, 'POST', headers, message) + + raise gen.Return(response) + + @gen.coroutine + def post_xml(self, address, envelope, headers): + message = etree_to_string(envelope) + + response = yield self.post(address, message, headers) + + raise gen.Return(response) + + @gen.coroutine + def get(self, address, params, headers): + if params: + address += '?' + urllib.urlencode(params) + response = yield self.fetch(address, 'GET', headers) + + raise gen.Return(response) + + @gen.coroutine + def fetch(self, address, method, headers, message=None): + async_client = httpclient.AsyncHTTPClient() + + # extracting auth + auth_username = None + auth_password = None + auth_mode = None + + if self.session.auth: + if type(self.session.auth) is tuple: + auth_username = self.session.auth[0] + auth_password = self.session.auth[1] + auth_mode = 'basic' + elif type(self.session.auth) is HTTPBasicAuth: + auth_username = self.session.username + auth_password = self.session.password + auth_mode = 'basic' + elif type(self.session.auth) is HTTPDigestAuth: + auth_username = self.session.username + auth_password = self.session.password + auth_mode = 'digest' + else: + raise StandardError('Not supported authentication.') + + # extracting client cert + client_cert = None + client_key = None + + if self.session.cert: + if type(self.session.cert) is str: + client_cert = self.session.cert + elif type(self.session.cert) is tuple: + client_cert = self.session.cert[0] + client_key = self.session.cert[1] + + session_headers = dict(self.session.headers.items()) + + kwargs = { + 'method': method, + 'request_timeout': self.operation_timeout, + 'headers': dict(headers, **session_headers), + 'auth_username': auth_username, + 'auth_password': auth_password, + 'auth_mode': auth_mode, + 'validate_cert': self.session.verify, + 'client_key': client_key, + 'client_cert': client_cert + } + + if message: + kwargs['body'] = message + + http_req = httpclient.HTTPRequest(address, **kwargs) + response = yield async_client.fetch(http_req) + + raise gen.Return(self.new_response(response)) + + def new_response(self, response): + """Convert an tornado.HTTPResponse object to a requests.Response object""" + new = Response() + new._content = response.body + new.status_code = response.code + new.headers = dict(response.headers.get_all()) + return new \ No newline at end of file diff --git a/src/zeep/transports.py b/src/zeep/transports.py index 29a4453..1f18772 100644 --- a/src/zeep/transports.py +++ b/src/zeep/transports.py @@ -19,7 +19,6 @@ class Transport(object): :param session: A :py:class:`request.Session()` object (optional) """ - supports_async = False def __init__(self, cache=None, timeout=300, operation_timeout=None, session=None): diff --git a/src/zeep/utils.py b/src/zeep/utils.py index 2d15c51..b763c8a 100644 --- a/src/zeep/utils.py +++ b/src/zeep/utils.py @@ -26,7 +26,8 @@ def as_qname(value, nsmap, target_namespace=None): namespace = nsmap.get(prefix) if not namespace: - raise XMLParseError("No namespace defined for %r" % prefix) + raise XMLParseError( + "No namespace defined for %r (%r)" % (prefix, value)) # Workaround for https://github.com/mvantellingen/python-zeep/issues/349 if not local: diff --git a/src/zeep/wsdl/attachments.py b/src/zeep/wsdl/attachments.py index 505980c..5ddd816 100644 --- a/src/zeep/wsdl/attachments.py +++ b/src/zeep/wsdl/attachments.py @@ -75,6 +75,6 @@ class Attachment(object): if encoding == 'base64': return base64.b64decode(content) elif encoding == 'binary': - return content + return content.strip(b'\r\n') else: return content diff --git a/src/zeep/wsdl/bindings/soap.py b/src/zeep/wsdl/bindings/soap.py index ae08ad7..59adf00 100644 --- a/src/zeep/wsdl/bindings/soap.py +++ b/src/zeep/wsdl/bindings/soap.py @@ -10,6 +10,7 @@ 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 +from zeep.wsdl.messages.xop import process_xop from zeep.wsdl.utils import etree_to_string, url_http_to_https logger = logging.getLogger(__name__) @@ -133,16 +134,18 @@ class SoapBinding(Binding): if response.status_code != 200 and not response.content: raise TransportError( u'Server returned HTTP status %d (no content available)' - % response.status_code) + % response.status_code, + status_code=response.status_code) content_type = response.headers.get('Content-Type', 'text/xml') media_type = get_media_type(content_type) message_pack = None + # If the reply is a multipart/related then we need to retrieve all the + # parts 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:]) @@ -157,7 +160,14 @@ class SoapBinding(Binding): except XMLSyntaxError: raise TransportError( 'Server returned HTTP status %d (%s)' - % (response.status_code, response.content)) + % (response.status_code, response.content), + status_code=response.status_code, + content=response.content) + + # Check if this is an XOP message which we need to decode first + if message_pack: + if process_xop(doc, message_pack): + message_pack = None if client.wsse: client.wsse.verify(doc) @@ -184,6 +194,9 @@ class SoapBinding(Binding): def process_service_port(self, xmlelement, force_https=False): address_node = xmlelement.find('soap:address', namespaces=self.nsmap) + if address_node is None: + logger.debug("No valid soap:address found for service") + return # Force the usage of HTTPS when the force_https boolean is true location = address_node.get('location') diff --git a/src/zeep/wsdl/messages/multiref.py b/src/zeep/wsdl/messages/multiref.py index 907049e..04abec3 100644 --- a/src/zeep/wsdl/messages/multiref.py +++ b/src/zeep/wsdl/messages/multiref.py @@ -1,4 +1,4 @@ -import copy +import re from lxml import etree @@ -19,6 +19,7 @@ def process_multiref(node): used_nodes = [] def process(node): + """Recursive""" # TODO (In Soap 1.2 this is 'ref') href = node.attrib.get('href') @@ -26,14 +27,7 @@ def process_multiref(node): 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 + node = _dereference_element(obj, node) for child in node: process(child) @@ -48,34 +42,117 @@ def process_multiref(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} + """Move the referenced node (source) in the main response tree (target) - new = etree.Element(target.tag, nsmap=specific_nsmap) + :type source: lxml.etree._Element + :type target: lxml.etree._Element + :rtype target: lxml.etree._Element - # 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 + """ + specific_nsmap = { + k: v for k, v in source.nsmap.items() if k not in target.nsmap + } - 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 + new = _clone_element(source, target.tag, specific_nsmap) - if not setted: - new.set(key, value) + # Replace the node with the new dereferenced node + parent = target.getparent() + parent.insert(parent.index(target), new) + parent.remove(target) - # Copy the children and the text content - for child in source: - new.append(copy.deepcopy(child)) - new.text = source.text + # Update all descendants + for obj in new.iter(): + _prefix_node(obj) return new + + +def _clone_element(node, tag_name=None, nsmap=None): + """Clone the given node and return it. + + This is a recursive call since we want to clone the children the same + way. + + :type source: lxml.etree._Element + :type tag_name: str + :type nsmap: dict + :rtype source: lxml.etree._Element + + """ + tag_name = tag_name or node.tag + nsmap = node.nsmap if nsmap is None else nsmap + new = etree.Element(tag_name, nsmap=nsmap) + + for child in node: + new_child = _clone_element(child) + new.append(new_child) + new.text = node.text + + for key, value in _get_attributes(node): + new.set(key, value) + + return new + + +def _prefix_node(node): + """Translate the internal attribute values back to prefixed tokens. + + This reverses the translation done in _get_attributes + + For example:: + + { + 'foo:type': '{http://example.com}string' + } + + will be converted to: + + { + 'foo:type': 'example:string' + } + + :type node: lxml.etree._Element + + """ + reverse_nsmap = {v: k for k, v in node.nsmap.items()} + + prefix_re = re.compile('^{([^}]+)}(.*)') + + for key, value in node.attrib.items(): + if value.startswith('{'): + match = prefix_re.match(value) + namespace, localname = match.groups() + + if namespace in reverse_nsmap: + value = '%s:%s' % (reverse_nsmap.get(namespace), localname) + node.set(key, value) + + +def _get_attributes(node): + """Return the node attributes where prefixed values are dereferenced. + + For example the following xml:: + + + + will return the dict:: + + { + 'foo:type': '{http://example.com}string' + } + + :type node: lxml.etree._Element + + """ + nsmap = node.nsmap + result = {} + + for key, value in node.attrib.items(): + if value.count(':') == 1: + prefix, localname = value.split(':') + + if prefix in nsmap: + namespace = nsmap[prefix] + value = '{%s}%s' % (namespace, localname) + result[key] = value + return list(result.items()) diff --git a/src/zeep/wsdl/messages/soap.py b/src/zeep/wsdl/messages/soap.py index 940a2e6..265f5d0 100644 --- a/src/zeep/wsdl/messages/soap.py +++ b/src/zeep/wsdl/messages/soap.py @@ -9,7 +9,6 @@ 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 @@ -53,24 +52,22 @@ class SoapMessage(ConcreteMessage): nsmap.update(self.wsdl.types._prefix_map_custom) soap = ElementMaker(namespace=self.nsmap['soap-env'], nsmap=nsmap) - body = header = None # Create the soap:header element headers_value = kwargs.pop('_soapheaders', None) header = self._serialize_header(headers_value, nsmap) # Create the soap:body element + body = soap.Body() if self.body: body_value = self.body(*args, **kwargs) - body = soap.Body() self.body.render(body, body_value) # Create the soap:envelope envelope = soap.Envelope() if header is not None: envelope.append(header) - if body is not None: - envelope.append(body) + envelope.append(body) # XXX: This is only used in Soap 1.1 so should be moved to the the # Soap11Binding._set_http_headers(). But let's keep it like this for @@ -89,7 +86,6 @@ class SoapMessage(ConcreteMessage): if not self.envelope: return None - body = envelope.find('soap-env:Body', namespaces=self.nsmap) body_result = self._deserialize_body(body) @@ -136,7 +132,10 @@ class SoapMessage(ConcreteMessage): return None return self.envelope.type.signature(schema=self.wsdl.types, standalone=False) - parts = [self.body.type.signature(schema=self.wsdl.types, standalone=False)] + if self.body: + parts = [self.body.type.signature(schema=self.wsdl.types, standalone=False)] + else: + parts = [] if self.header.type._element: parts.append('_soapheaders={%s}' % self.header.type.signature( schema=self.wsdl.types, standalone=False)) @@ -301,7 +300,9 @@ class SoapMessage(ConcreteMessage): xsd.Element('{%s}header' % self.nsmap['soap-env'], self.header.type)) all_elements.append( - xsd.Element('{%s}body' % self.nsmap['soap-env'], self.body.type)) + xsd.Element( + '{%s}body' % self.nsmap['soap-env'], + self.body.type if self.body else None)) return xsd.Element('{%s}envelope' % self.nsmap['soap-env'], xsd.ComplexType(all_elements)) @@ -414,7 +415,7 @@ class DocumentMessage(SoapMessage): name = etree.QName(self.nsmap['soap-env'], 'Body') if not info or not parts: - return xsd.Element(name, xsd.ComplexType([])) + return None # If the part name is omitted then all parts are available under # the soap:body tag. Otherwise only the part with the given name. @@ -470,9 +471,8 @@ class RpcMessage(SoapMessage): name and its namespace is the value of the namespace attribute. """ - name = etree.QName(self.nsmap['soap-env'], 'Body') if not info: - return xsd.Element(name, xsd.ComplexType([])) + return None namespace = info['namespace'] if self.type == 'input': diff --git a/src/zeep/wsdl/messages/xop.py b/src/zeep/wsdl/messages/xop.py new file mode 100644 index 0000000..b62645b --- /dev/null +++ b/src/zeep/wsdl/messages/xop.py @@ -0,0 +1,26 @@ +import base64 + + +def process_xop(document, message_pack): + """Iterate through the tree and replace the xop:include elements.""" + + xop_nodes = document.xpath('//xop:Include', namespaces={ + 'xop': 'http://www.w3.org/2004/08/xop/include' + }) + num_replaced = 0 + + for xop_node in xop_nodes: + href = xop_node.get('href') + if href.startswith('cid:'): + href = '<%s>' % href[4:] + + value = message_pack.get_by_content_id(href) + if not value: + raise ValueError("No part found for: %r" % xop_node.get('href')) + num_replaced += 1 + + xop_parent = xop_node.getparent() + xop_parent.remove(xop_node) + xop_parent.text = base64.b64encode(value.content) + + return num_replaced > 0 diff --git a/src/zeep/wsdl/parse.py b/src/zeep/wsdl/parse.py index f2b1506..d3b8eee 100644 --- a/src/zeep/wsdl/parse.py +++ b/src/zeep/wsdl/parse.py @@ -34,6 +34,7 @@ def parse_abstract_message(wsdl, xmlelement): """ tns = wsdl.target_namespace + message_name = qname_attr(xmlelement, 'name', tns) parts = [] for part in xmlelement.findall('wsdl:part', namespaces=NSMAP): @@ -49,15 +50,14 @@ def parse_abstract_message(wsdl, xmlelement): except (NamespaceError, LookupError): raise IncompleteMessage(( - "The wsdl:message for %r contains " - "invalid xsd types or elements" - ) % part_name) + "The wsdl:message for %r contains an invalid part (%r): " + "invalid xsd type or elements" + ) % (message_name.text, part_name)) part = definitions.MessagePart(part_element, part_type) parts.append((part_name, part)) # Create the object, add the parts and return it - message_name = qname_attr(xmlelement, 'name', tns) msg = definitions.AbstractMessage(message_name) for part_name, part in parts: msg.add_part(part_name, part) diff --git a/src/zeep/wsdl/utils.py b/src/zeep/wsdl/utils.py index e73ac20..37537e2 100644 --- a/src/zeep/wsdl/utils.py +++ b/src/zeep/wsdl/utils.py @@ -23,7 +23,7 @@ def get_or_create_header(envelope): def etree_to_string(node): return etree.tostring( - node, pretty_print=True, xml_declaration=True, encoding='utf-8') + node, pretty_print=False, xml_declaration=True, encoding='utf-8') def url_http_to_https(value): diff --git a/src/zeep/wsdl/wsdl.py b/src/zeep/wsdl/wsdl.py index bc43806..7a32d1e 100644 --- a/src/zeep/wsdl/wsdl.py +++ b/src/zeep/wsdl/wsdl.py @@ -389,7 +389,8 @@ class Definition(object): """ result = {} - if not getattr(self.wsdl.transport, 'supports_async', False): + + if not getattr(self.wsdl.transport, 'binding_classes', None): from zeep.wsdl import bindings binding_classes = [ bindings.Soap11Binding, @@ -398,11 +399,7 @@ class Definition(object): bindings.HttpPostBinding, ] else: - from zeep.asyncio import bindings # Python 3.5+ syntax - binding_classes = [ - bindings.AsyncSoap11Binding, - bindings.AsyncSoap12Binding, - ] + binding_classes = self.wsdl.transport.binding_classes for binding_node in doc.findall('wsdl:binding', namespaces=NSMAP): # Detect the binding type diff --git a/src/zeep/wsse/signature.py b/src/zeep/wsse/signature.py index ccc8e18..e2d448b 100644 --- a/src/zeep/wsse/signature.py +++ b/src/zeep/wsse/signature.py @@ -25,22 +25,23 @@ except ImportError: # 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() + 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) + 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) + 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.""" @@ -61,13 +62,14 @@ class MemorySignature(object): _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) + super(Signature, self).__init__( + _read_file(key_file), _read_file(certfile), password) + def check_xmlsec_import(): if xmlsec is None: @@ -170,7 +172,9 @@ def sign_envelope(envelope, keyfile, certfile, password=None): 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): + soap_env = detect_soap_env(envelope) # Create the Signature node. signature = xmlsec.template.create( @@ -189,17 +193,13 @@ def _sign_envelope_with_key(envelope, key): # Insert the Signature node in the wsse:Security header. security = get_security_header(envelope) security.insert(0, signature) + security.append(etree.Element(QName(ns.WSU, 'Timestamp'))) # Perform the actual signing. ctx = xmlsec.SignatureContext() ctx.key = key - - security.append(etree.Element(QName(ns.WSU, 'Timestamp'))) - - soap_env = detect_soap_env(envelope) _sign_node(ctx, signature, envelope.find(QName(soap_env, 'Body'))) _sign_node(ctx, signature, security.find(QName(ns.WSU, 'Timestamp'))) - ctx.sign(signature) # Place the X509 data inside a WSSE SecurityTokenReference within @@ -223,11 +223,12 @@ def verify_envelope(envelope, certfile): 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: + if header is None: raise SignatureVerificationFailed() security = header.find(QName(ns.WSSE, 'Security')) diff --git a/src/zeep/xsd/const.py b/src/zeep/xsd/const.py index 75b4097..c8354cc 100644 --- a/src/zeep/xsd/const.py +++ b/src/zeep/xsd/const.py @@ -2,6 +2,7 @@ from lxml import etree from zeep import ns + def xsi_ns(localname): return etree.QName(ns.XSI, localname) @@ -21,3 +22,8 @@ class _StaticIdentity(object): NotSet = _StaticIdentity('NotSet') SkipValue = _StaticIdentity('SkipValue') Nil = _StaticIdentity('Nil') + + +AUTO_IMPORT_NAMESPACES = [ + 'http://schemas.xmlsoap.org/soap/encoding/' +] diff --git a/src/zeep/xsd/elements/any.py b/src/zeep/xsd/elements/any.py index e799191..7e01d82 100644 --- a/src/zeep/xsd/elements/any.py +++ b/src/zeep/xsd/elements/any.py @@ -183,7 +183,7 @@ class Any(Base): if self.restrict: expected_types = (etree._Element, dict,) + self.restrict.accepted_types else: - expected_types = (etree._Element, dict,AnyObject) + expected_types = (etree._Element, dict, AnyObject) if not isinstance(value, expected_types): type_names = [ diff --git a/src/zeep/xsd/elements/indicators.py b/src/zeep/xsd/elements/indicators.py index 10ccf00..601affe 100644 --- a/src/zeep/xsd/elements/indicators.py +++ b/src/zeep/xsd/elements/indicators.py @@ -629,7 +629,7 @@ class Group(Indicator): super(Group, self).__init__() self.child = child self.qname = name - self.name = name.localname + self.name = name.localname if name else None self.max_occurs = max_occurs self.min_occurs = min_occurs @@ -648,7 +648,7 @@ class Group(Indicator): def clone(self, name, min_occurs=1, max_occurs=1): return self.__class__( - name=name, + name=None, child=self.child, min_occurs=min_occurs, max_occurs=max_occurs) diff --git a/src/zeep/xsd/schema.py b/src/zeep/xsd/schema.py index 8026009..2e86147 100644 --- a/src/zeep/xsd/schema.py +++ b/src/zeep/xsd/schema.py @@ -4,6 +4,8 @@ from collections import OrderedDict from lxml import etree from zeep import exceptions, ns +from zeep.loader import load_external +from zeep.xsd import const 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 @@ -115,6 +117,15 @@ class Schema(object): self._prefix_map_auto = self._create_prefix_map() + def add_document_by_url(self, url): + schema_node = load_external( + url, + self._transport, + strict=self.strict) + + document = self.create_new_document(schema_node, url=url) + document.resolve() + def get_element(self, qname): """Return a global xsd.Element object with the given qname @@ -304,6 +315,13 @@ class Schema(object): :rtype: list of SchemaDocument """ + if ( + namespace not in self._documents + and namespace in const.AUTO_IMPORT_NAMESPACES + ): + logger.debug("Auto importing missing known schema: %s", namespace) + self.add_document_by_url(namespace) + if namespace not in self._documents: if fail_silently: return [] @@ -384,7 +402,6 @@ class SchemaDocument(object): "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, diff --git a/src/zeep/xsd/types/builtins.py b/src/zeep/xsd/types/builtins.py index 0bbe5d9..08cd9a6 100644 --- a/src/zeep/xsd/types/builtins.py +++ b/src/zeep/xsd/types/builtins.py @@ -109,7 +109,12 @@ class Duration(BuiltinType, AnySimpleType): return isodate.duration_isoformat(value) def pythonvalue(self, value): - return isodate.parse_duration(value) + if value.startswith('PT-'): + value = value.replace('PT-', 'PT') + result = isodate.parse_duration(value) + return datetime.timedelta(0 - result.total_seconds()) + else: + return isodate.parse_duration(value) class DateTime(BuiltinType, AnySimpleType): @@ -142,6 +147,9 @@ class Time(BuiltinType, AnySimpleType): @check_no_collection def xmlvalue(self, value): + if isinstance(value, six.string_types): + return value + if value.microsecond: return isodate.isostrf.strftime(value, '%H:%M:%S.%f%Z') return isodate.isostrf.strftime(value, '%H:%M:%S%Z') diff --git a/src/zeep/xsd/types/complex.py b/src/zeep/xsd/types/complex.py index 7e65a70..d65a57f 100644 --- a/src/zeep/xsd/types/complex.py +++ b/src/zeep/xsd/types/complex.py @@ -13,7 +13,7 @@ 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, ArrayValue +from zeep.xsd.valueobjects import ArrayValue, CompoundValue logger = logging.getLogger(__name__) @@ -212,6 +212,10 @@ class ComplexType(AnyType): if not self.elements_nested and not self.attributes: return + # TODO: Implement test case for this + if value is None: + value = {} + if isinstance(value, ArrayValue): value = value.as_value_object() @@ -377,6 +381,9 @@ class ComplexType(AnyType): elif isinstance(element, OrderIndicator): for item in reversed(base_element): element.insert(0, item) + elif isinstance(element, Group): + for item in reversed(base_element): + element.child.insert(0, item) elif isinstance(self._element, Group): raise NotImplementedError('TODO') diff --git a/src/zeep/xsd/visitor.py b/src/zeep/xsd/visitor.py index 9ba0101..a9e47c5 100644 --- a/src/zeep/xsd/visitor.py +++ b/src/zeep/xsd/visitor.py @@ -9,7 +9,7 @@ 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.const import AUTO_IMPORT_NAMESPACES, xsd_ns from zeep.xsd.types.unresolved import UnresolvedCustomType, UnresolvedType logger = logging.getLogger(__name__) @@ -1138,9 +1138,11 @@ class SchemaVisitor(object): # that fact and handle it by auto-importing the schema if it is # referenced. if ( - name.namespace == 'http://schemas.xmlsoap.org/soap/encoding/' and - not self.document.is_imported(name.namespace) + name.namespace in AUTO_IMPORT_NAMESPACES + and not self.document.is_imported(name.namespace) ): + logger.debug( + "Auto importing missing known schema: %s", name.namespace) import_node = etree.Element( tags.import_, namespace=name.namespace, schemaLocation=name.namespace) diff --git a/tests/test_asyncio_transport.py b/tests/test_asyncio_transport.py index 7aca012..0032251 100644 --- a/tests/test_asyncio_transport.py +++ b/tests/test_asyncio_transport.py @@ -4,7 +4,7 @@ from lxml import etree import aiohttp from aioresponses import aioresponses -from zeep import cache, asyncio +from zeep import cache, asyncio, exceptions @pytest.mark.requests @@ -58,3 +58,19 @@ async def test_session_no_close(event_loop): transport = asyncio.AsyncTransport(loop=event_loop, session=session) del transport assert not session.closed + + +@pytest.mark.requests +def test_http_error(event_loop): + transport = asyncio.AsyncTransport(loop=event_loop) + + with aioresponses() as m: + m.get( + 'http://tests.python-zeep.org/test.xml', + body='x', + status=500, + ) + with pytest.raises(exceptions.TransportError) as exc: + transport.load('http://tests.python-zeep.org/test.xml') + assert exc.value.status_code == 500 + assert exc.value.message is None diff --git a/tests/test_client.py b/tests/test_client.py index 6a45e85..9803f08 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -183,6 +183,20 @@ def test_set_context_options_timeout(): assert obj.transport.operation_timeout is None +def test_set_context_options_raw_response(): + obj = client.Client('tests/wsdl_files/soap.wsdl') + + assert obj.raw_response is False + with obj.options(raw_response=True): + assert obj.raw_response is True + + with obj.options(): + # Check that raw_response is not changed by default value + assert obj.raw_response is True + # Check that the original value returned + assert obj.raw_response is False + + @pytest.mark.requests def test_default_soap_headers(): header = xsd.ComplexType( diff --git a/tests/test_soap_multiref.py b/tests/test_soap_multiref.py index ae9ab7e..9cbcebc 100644 --- a/tests/test_soap_multiref.py +++ b/tests/test_soap_multiref.py @@ -12,7 +12,7 @@ from zeep.transports import Transport @pytest.mark.requests -def test_parse_soap_wsdl(): +def test_parse_multiref_soap_response(): wsdl_file = io.StringIO(u""" + xmlns:tns="http://tests.python-zeep.org/" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> @@ -132,3 +133,135 @@ def test_parse_soap_wsdl(): 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' + + + +@pytest.mark.requests +def test_parse_multiref_soap_response_child(): + wsdl_file = io.StringIO(u""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test service + + + + + + """.strip()) + + content = """ + + + + + + + + + foo + bar + + bar + + + + + + foo + bar + + + + + """.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' + diff --git a/tests/test_soap_xop.py b/tests/test_soap_xop.py new file mode 100644 index 0000000..03f5ebd --- /dev/null +++ b/tests/test_soap_xop.py @@ -0,0 +1,253 @@ +import io +from requests_toolbelt.multipart.decoder import MultipartDecoder +from pretend import stub +from lxml import etree +from tests.utils import load_xml, assert_nodes_equal +from zeep.wsdl.attachments import MessagePack + + +from zeep.wsdl.messages import xop + + +def test_rebuild_xml(): + data = '\r\n'.join(line.strip() for line in """ + --MIME_boundary + Content-Type: application/soap+xml; charset=UTF-8 + Content-Transfer-Encoding: 8bit + Content-ID: + + + + + 5XJ45-3B2 + accident + + + + + + --MIME_boundary + Content-Type: image/jpeg + Content-Transfer-Encoding: binary + Content-ID: + + ...binary JPG image... + + --MIME_boundary-- + """.splitlines()).encode('utf-8') + + response = stub( + status_code=200, + content=data, + encoding=None, + headers={ + 'Content-Type': 'multipart/related; boundary=MIME_boundary; type="application/soap+xml"; start="" 1' + } + ) + client = stub( + transport=None, + wsdl=stub(strict=True), + xml_huge_tree=False) + + + decoder = MultipartDecoder( + response.content, response.headers['Content-Type'], 'utf-8') + + document = etree.fromstring(decoder.parts[0].content) + message_pack = MessagePack(parts=decoder.parts[1:]) + xop.process_xop(document, message_pack) + + expected = """ + + + + 5XJ45-3B2 + accident + Li4uYmluYXJ5IEpQRyBpbWFnZS4uLg== + + + + """ + assert_nodes_equal(etree.tostring(document), expected) + + + +import pytest +import requests_mock + +from six import StringIO + +from zeep import Client +from zeep.transports import Transport + + +def test_xop(): + wsdl_main = StringIO(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test service + + + + + + """.strip()) + + client = Client(wsdl_main, transport=Transport()) + service = client.create_service( + "{http://tests.python-zeep.org/xsd-main}TestBinding", + "http://tests.python-zeep.org/test") + + content_type = 'multipart/related; boundary="boundary"; type="application/xop+xml"; start=""; start-info="application/soap+xml; charset=utf-8"' + + response1 = '\r\n'.join(line.strip() for line in """ + Content-Type: application/xop+xml; charset=utf-8; type="application/soap+xml" + Content-Transfer-Encoding: binary + Content-ID: + + + + + + + + + + + + --boundary + Content-Type: application/binary + Content-Transfer-Encoding: binary + Content-ID: + + BINARYDATA + --boundary-- + """.splitlines()) + + response2 = '\r\n'.join(line.strip() for line in """ + Content-Type: application/xop+xml; charset=utf-8; type="application/soap+xml" + Content-Transfer-Encoding: binary + Content-ID: + + + + + + + + + + + + --boundary + Content-Type: application/binary + Content-Transfer-Encoding: binary + Content-ID: + + BINARYDATA + + --boundary-- + """.splitlines()) + + print(response1) + with requests_mock.mock() as m: + m.post('http://tests.python-zeep.org/test', + content=response2.encode("utf-8"), + headers={"Content-Type": content_type}) + result = service.TestOperation2("") + assert result["_value_1"] == "BINARYDATA".encode() + + m.post( + 'http://tests.python-zeep.org/test', + content=response1.encode("utf-8"), + headers={"Content-Type": content_type}) + result = service.TestOperation1("") + assert result == "BINARYDATA".encode() + + diff --git a/tests/test_tornado_transport.py b/tests/test_tornado_transport.py new file mode 100644 index 0000000..0362589 --- /dev/null +++ b/tests/test_tornado_transport.py @@ -0,0 +1,57 @@ +import pytest +from pretend import stub +from lxml import etree +from tornado.httpclient import HTTPResponse, HTTPRequest +from tornado.testing import gen_test, AsyncTestCase +from tornado.concurrent import Future + +from mock import patch +from zeep.tornado import TornadoAsyncTransport + + +class TornadoAsyncTransportTest(AsyncTestCase): + @pytest.mark.requests + def test_no_cache(self): + transport = TornadoAsyncTransport() + assert transport.cache is None + + @pytest.mark.requests + @patch('tornado.httpclient.HTTPClient.fetch') + @gen_test + def test_load(self, mock_httpclient_fetch): + cache = stub(get=lambda url: None, add=lambda url, content: None) + response = HTTPResponse(HTTPRequest('http://tests.python-zeep.org/test.xml'), 200) + response.buffer = True + response._body = 'x' + mock_httpclient_fetch.return_value = response + + transport = TornadoAsyncTransport(cache=cache) + + result = transport.load('http://tests.python-zeep.org/test.xml') + + assert result == 'x' + + @pytest.mark.requests + @patch('tornado.httpclient.AsyncHTTPClient.fetch') + @gen_test + def test_post(self, mock_httpclient_fetch): + cache = stub(get=lambda url: None, add=lambda url, content: None) + + response = HTTPResponse(HTTPRequest('http://tests.python-zeep.org/test.xml'), 200) + response.buffer = True + response._body = 'x' + http_fetch_future = Future() + http_fetch_future.set_result(response) + mock_httpclient_fetch.return_value = http_fetch_future + + transport = TornadoAsyncTransport(cache=cache) + + envelope = etree.Element('Envelope') + + result = yield transport.post_xml( + 'http://tests.python-zeep.org/test.xml', + envelope=envelope, + headers={}) + + assert result.content == 'x' + assert result.status_code == 200 diff --git a/tests/test_wsdl_messages_document.py b/tests/test_wsdl_messages_document.py index 151aef3..0b9f91c 100644 --- a/tests/test_wsdl_messages_document.py +++ b/tests/test_wsdl_messages_document.py @@ -1267,3 +1267,68 @@ def test_serialize_any_type(): deserialized = operation.input.deserialize(serialized.content) assert deserialized == 'ah1' + + +def test_empty_input_parse(): + wsdl_content = StringIO(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """.strip()) + + root = wsdl.Document(wsdl_content, None) + + binding = root.bindings['{http://tests.python-zeep.org/}Binding'] + operation = binding.get('getResult') + assert operation.input.signature() == '' + + serialized = operation.input.serialize() + expected = """ + + + + + """ + assert_nodes_equal(expected, serialized.content) diff --git a/tests/test_wsdl_soap.py b/tests/test_wsdl_soap.py index e80c152..506f603 100644 --- a/tests/test_wsdl_soap.py +++ b/tests/test_wsdl_soap.py @@ -183,9 +183,11 @@ def test_wrong_content(): headers={} ) - with pytest.raises(TransportError): + with pytest.raises(TransportError) as exc: binding.process_reply( client, binding.get('GetLastTradePrice'), response) + assert 200 == exc.value.status_code + assert data == exc.value.content def test_wrong_no_unicode_content(): @@ -204,10 +206,35 @@ def test_wrong_no_unicode_content(): headers={} ) - with pytest.raises(TransportError): + with pytest.raises(TransportError) as exc: binding.process_reply( client, binding.get('GetLastTradePrice'), response) + assert 200 == exc.value.status_code + assert data == exc.value.content + + +def test_http_error(): + data = """ + Unauthorized! + """.strip() + + client = Client('tests/wsdl_files/soap.wsdl') + binding = client.service._binding + + response = stub( + status_code=401, + content=data, + encoding='utf-8', + headers={} + ) + + with pytest.raises(TransportError) as exc: + binding.process_reply( + client, binding.get('GetLastTradePrice'), response) + assert 401 == exc.value.status_code + assert data == exc.value.content + def test_mime_multipart(): data = '\r\n'.join(line.strip() for line in """ diff --git a/tests/test_wsse_signature.py b/tests/test_wsse_signature.py index 503e90e..9b910d1 100644 --- a/tests/test_wsse_signature.py +++ b/tests/test_wsse_signature.py @@ -2,10 +2,11 @@ import os import sys import pytest +from lxml import etree from tests.utils import load_xml -from zeep.exceptions import SignatureVerificationFailed from zeep import wsse +from zeep.exceptions import SignatureVerificationFailed from zeep.wsse import signature DS_NS = 'http://www.w3.org/2000/09/xmldsig#' diff --git a/tests/test_xsd_builtins.py b/tests/test_xsd_builtins.py index 17ae68e..6164732 100644 --- a/tests/test_xsd_builtins.py +++ b/tests/test_xsd_builtins.py @@ -151,6 +151,7 @@ class TestTime: instance = builtins.Time() value = datetime.time(21, 14, 42) assert instance.xmlvalue(value) == '21:14:42' + assert instance.xmlvalue("21:14:42") == '21:14:42' def test_pythonvalue(self): instance = builtins.Time() diff --git a/tests/test_xsd_indicators_group.py b/tests/test_xsd_indicators_group.py index 68d5924..4f35eb0 100644 --- a/tests/test_xsd_indicators_group.py +++ b/tests/test_xsd_indicators_group.py @@ -415,3 +415,39 @@ def test_xml_group_methods(): '{http://tests.python-zeep.org/}Group(city: xsd:string, country: xsd:string)') assert len(list(Group)) == 2 + + +def test_xml_group_extension(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + + + + + + + + """)) + SubGroup = schema.get_type('{http://tests.python-zeep.org/}SubGroup') + assert SubGroup.signature(schema) == ( + 'ns0:SubGroup(item_1: xsd:string, item_2: xsd:string, item_3: xsd:string)') + SubGroup(item_1='een', item_2='twee', item_3='drie')