Merging upstream version 2.4.0.

This commit is contained in:
Mathias Behrle 2017-09-04 16:23:14 +02:00
parent eb954e9b3f
commit 9ee5ee5a7c
45 changed files with 1133 additions and 131 deletions

26
CHANGES
View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.1.1
current_version = 2.4.0
commit = true
tag = true
tag_name = {new_version}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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::
<foobar xmlns:xsi="foo" xmlns:ns0="bar" xsi:type="ns0:string">
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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"""
<?xml version="1.0"?>
<wsdl:definitions
@ -94,7 +94,8 @@ def test_parse_soap_wsdl():
<?xml version="1.0"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://tests.python-zeep.org/">
xmlns:tns="http://tests.python-zeep.org/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<tns:TestOperationResponse>
<tns:output>
@ -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"""
<?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:element name="subitem_3" type="tns:type_3"/>
</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:complexType name="type_3" nillable="true">
<xsd:sequence>
</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/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<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>
<tns:subitem_3 xmlns:tns2="http://tests.python-zeep.org/" xsi:type="tns2:type_3"></tns:subitem_3>
</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'

253
tests/test_soap_xop.py Normal file
View File

@ -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: <claim@insurance.com>
<soap:Envelope
xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:xop='http://www.w3.org/2004/08/xop/include'
xmlns:xop-mime='http://www.w3.org/2005/05/xmlmime'>
<soap:Body>
<submitClaim>
<accountNumber>5XJ45-3B2</accountNumber>
<eventType>accident</eventType>
<image xop-mime:content-type='image/jpeg'><xop:Include href="cid:image@insurance.com"/></image>
</submitClaim>
</soap:Body>
</soap:Envelope>
--MIME_boundary
Content-Type: image/jpeg
Content-Transfer-Encoding: binary
Content-ID: <image@insurance.com>
...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="<claim@insurance.com>" 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 = """
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:xop-mime="http://www.w3.org/2005/05/xmlmime">
<soap:Body>
<submitClaim>
<accountNumber>5XJ45-3B2</accountNumber>
<eventType>accident</eventType>
<image xop-mime:content-type="image/jpeg">Li4uYmluYXJ5IEpQRyBpbWFnZS4uLg==</image>
</submitClaim>
</soap:Body>
</soap:Envelope>
"""
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("""
<?xml version="1.0"?>
<wsdl:definitions
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tests.python-zeep.org/xsd-main"
xmlns:sec="http://tests.python-zeep.org/wsdl-secondary"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
targetNamespace="http://tests.python-zeep.org/xsd-main">
<wsdl:types>
<xsd:schema
targetNamespace="http://tests.python-zeep.org/xsd-main"
xmlns:tns="http://tests.python-zeep.org/xsd-main">
<xsd:complexType name="responseTypeSimple">
<xsd:sequence>
<xsd:element name="BinaryData" type="xsd:base64Binary"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="BinaryDataType">
<xsd:simpleContent>
<xsd:extension base="xsd:base64Binary">
<xsd:anyAttribute namespace="##other" processContents="lax"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="responseTypeComplex">
<xsd:sequence>
<xsd:element name="BinaryData" type="tns:BinaryDataType"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="input" type="xsd:string"/>
<xsd:element name="resultSimple" type="tns:responseTypeSimple"/>
<xsd:element name="resultComplex" type="tns:responseTypeComplex"/>
</xsd:schema>
</wsdl:types>
<wsdl:message name="dummyRequest">
<wsdl:part name="response" element="tns:input"/>
</wsdl:message>
<wsdl:message name="dummyResponseSimple">
<wsdl:part name="response" element="tns:resultSimple"/>
</wsdl:message>
<wsdl:message name="dummyResponseComplex">
<wsdl:part name="response" element="tns:resultComplex"/>
</wsdl:message>
<wsdl:portType name="TestPortType">
<wsdl:operation name="TestOperation1">
<wsdl:input message="dummyRequest"/>
<wsdl:output message="dummyResponseSimple"/>
</wsdl:operation>
<wsdl:operation name="TestOperation2">
<wsdl:input message="dummyRequest"/>
<wsdl:output message="dummyResponseComplex"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="TestBinding" type="tns:TestPortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="TestOperation1">
<soap:operation soapAction="urn:dummyRequest"/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
<wsdl:operation name="TestOperation2">
<soap:operation soapAction="urn:dummyRequest"/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<soap:body use="literal"/>
</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())
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="<soap:Envelope>"; 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: <soap:Envelope>
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xop="http://www.w3.org/2004/08/xop/include"
xmlns:test="http://tests.python-zeep.org/xsd-main">
<soap:Body>
<test:resultSimple>
<test:BinaryData>
<xop:Include href="cid:id4"/>
</test:BinaryData>
</test:resultSimple>
</soap:Body>
</soap:Envelope>
--boundary
Content-Type: application/binary
Content-Transfer-Encoding: binary
Content-ID: <id4>
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: <soap:Envelope>
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xop="http://www.w3.org/2004/08/xop/include"
xmlns:test="http://tests.python-zeep.org/xsd-main">
<soap:Body>
<test:resultComplex>
<test:BinaryData>
<xop:Include href="cid:id4"/>
</test:BinaryData>
</test:resultComplex>
</soap:Body>
</soap:Envelope>
--boundary
Content-Type: application/binary
Content-Transfer-Encoding: binary
Content-ID: <id4>
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()

View File

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

View File

@ -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("""
<wsdl:definitions
xmlns:tns="http://tests.python-zeep.org/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://tests.python-zeep.org/">
<wsdl:types>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="http://tests.python-zeep.org/">
<element name="Result">
<complexType>
<sequence>
<element name="item" type="xsd:string"/>
</sequence>
</complexType>
</element>
</schema>
</wsdl:types>
<wsdl:message name="Request"></wsdl:message>
<wsdl:message name="Response">
<wsdl:part element="tns:Result" name="Result"/>
</wsdl:message>
<wsdl:portType name="PortType">
<wsdl:operation name="getResult">
<wsdl:input message="tns:Request" name="getResultRequest"/>
<wsdl:output message="tns:Response" name="getResultResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="Binding" type="tns:PortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getResult">
<soap:operation soapAction=""/>
<wsdl:input name="Result">
<soap:body use="literal"/>
</wsdl:input>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="Service">
<wsdl:port binding="tns:Binding" name="ActiveStations">
<soap:address location="https://opendap.co-ops.nos.noaa.gov/axis/services/ActiveStations"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
""".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 = """
<?xml version="1.0"?>
<soap-env:Envelope
xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body/>
</soap-env:Envelope>
"""
assert_nodes_equal(expected, serialized.content)

View File

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

View File

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

View File

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

View File

@ -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("""
<?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:sequence>
<xs:element name="item_2" type="xs:string" />
<xs:element name="item_3" type="xs:string" />
</xs:sequence>
</xs:group>
<xs:complexType name="base">
<xs:sequence>
<xs:element name="item_1" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="SubGroup">
<xs:complexContent>
<xs:extension base="tns:base">
<xs:group ref="tns:Group"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:schema>
"""))
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')