490 lines
18 KiB
Python
490 lines
18 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published by the
|
|
# Free Software Foundation; either version 3, or (at your option) any later
|
|
# version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
|
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
# for more details.
|
|
|
|
"""Pythonic simple SOAP Client helpers"""
|
|
|
|
|
|
from __future__ import unicode_literals
|
|
import sys
|
|
if sys.version > '3':
|
|
basestring = unicode = str
|
|
|
|
import datetime
|
|
from decimal import Decimal
|
|
import os
|
|
import logging
|
|
import hashlib
|
|
import warnings
|
|
|
|
try:
|
|
import urllib2
|
|
from urlparse import urlsplit
|
|
except ImportError:
|
|
from urllib import request as urllib2
|
|
from urllib.parse import urlsplit
|
|
|
|
from . import __author__, __copyright__, __license__, __version__
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''):
|
|
"""Download a document from a URL, save it locally if cache enabled"""
|
|
|
|
# check / append a valid schema if not given:
|
|
url_scheme, netloc, path, query, fragment = urlsplit(url)
|
|
if not url_scheme in ('http', 'https', 'file'):
|
|
for scheme in ('http', 'https', 'file'):
|
|
try:
|
|
if not url.startswith("/") and scheme in ('http', 'https'):
|
|
tmp_url = "%s://%s" % (scheme, os.path.join(wsdl_basedir, url))
|
|
else:
|
|
tmp_url = "%s:%s" % (scheme, os.path.join(wsdl_basedir, url))
|
|
log.debug('Scheme not found, trying %s' % scheme)
|
|
return fetch(tmp_url, http, cache, force_download, wsdl_basedir)
|
|
except Exception as e:
|
|
log.error(e)
|
|
raise RuntimeError('No scheme given for url: %s' % url)
|
|
|
|
# make md5 hash of the url for caching...
|
|
filename = '%s.xml' % hashlib.md5(url.encode('utf8')).hexdigest()
|
|
if isinstance(cache, basestring):
|
|
filename = os.path.join(cache, filename)
|
|
if cache and os.path.exists(filename) and not force_download:
|
|
log.info('Reading file %s' % filename)
|
|
f = open(filename, 'r')
|
|
xml = f.read()
|
|
f.close()
|
|
else:
|
|
if url_scheme == 'file':
|
|
log.info('Fetching url %s using urllib2' % url)
|
|
f = urllib2.urlopen(url)
|
|
xml = f.read()
|
|
else:
|
|
log.info('GET %s using %s' % (url, http._wrapper_version))
|
|
response, xml = http.request(url, 'GET', None, {})
|
|
if cache:
|
|
log.info('Writing file %s' % filename)
|
|
if not os.path.isdir(cache):
|
|
os.makedirs(cache)
|
|
f = open(filename, 'w')
|
|
f.write(xml)
|
|
f.close()
|
|
return xml
|
|
|
|
|
|
def sort_dict(od, d):
|
|
"""Sort parameters (same order as xsd:sequence)"""
|
|
if isinstance(od, dict):
|
|
ret = OrderedDict()
|
|
for k in od.keys():
|
|
v = d.get(k)
|
|
# don't append null tags!
|
|
if v is not None:
|
|
if isinstance(v, dict):
|
|
v = sort_dict(od[k], v)
|
|
elif isinstance(v, list):
|
|
v = [sort_dict(od[k][0], v1) for v1 in v]
|
|
ret[k] = v
|
|
if hasattr(od, 'namespace'):
|
|
ret.namespace = od.namespace
|
|
ret.qualified = od.qualified
|
|
return ret
|
|
else:
|
|
return d
|
|
|
|
|
|
def make_key(element_name, element_type, namespace):
|
|
"""Return a suitable key for elements"""
|
|
# only distinguish 'element' vs other types
|
|
if element_type in ('complexType', 'simpleType'):
|
|
eltype = 'complexType'
|
|
else:
|
|
eltype = element_type
|
|
if eltype not in ('element', 'complexType', 'simpleType'):
|
|
raise RuntimeError("Unknown element type %s = %s" % (element_name, eltype))
|
|
return (element_name, eltype, namespace)
|
|
|
|
|
|
def process_element(elements, element_name, node, element_type, xsd_uri, dialect, namespace, qualified=None,
|
|
soapenc_uri = 'http://schemas.xmlsoap.org/soap/encoding/'):
|
|
"""Parse and define simple element types"""
|
|
|
|
log.debug('Processing element %s %s' % (element_name, element_type))
|
|
for tag in node:
|
|
if tag.get_local_name() in ('annotation', 'documentation'):
|
|
continue
|
|
elif tag.get_local_name() in ('element', 'restriction'):
|
|
log.debug('%s has no children! %s' % (element_name, tag))
|
|
children = tag # element "alias"?
|
|
alias = True
|
|
elif tag.children():
|
|
children = tag.children()
|
|
alias = False
|
|
else:
|
|
log.debug('%s has no children! %s' % (element_name, tag))
|
|
continue # TODO: abstract?
|
|
d = OrderedDict()
|
|
d.namespace = namespace
|
|
d.qualified = qualified
|
|
for e in children:
|
|
t = e['type']
|
|
if not t:
|
|
t = e['base'] # complexContent (extension)!
|
|
if not t:
|
|
t = e['ref'] # reference to another element
|
|
if not t:
|
|
# "anonymous" elements had no type attribute but children
|
|
if e['name'] and e.children():
|
|
# create a type name to process the children
|
|
t = "%s_%s" % (element_name, e['name'])
|
|
c = e.children()
|
|
et = c.get_local_name()
|
|
c = c.children()
|
|
process_element(elements, t, c, et, xsd_uri, dialect, namespace, qualified)
|
|
else:
|
|
t = 'anyType' # no type given!
|
|
t = t.split(":")
|
|
if len(t) > 1:
|
|
ns, type_name = t
|
|
else:
|
|
ns, type_name = None, t[0]
|
|
if element_name == type_name and not alias and len(children) > 1:
|
|
continue # abort to prevent infinite recursion
|
|
uri = ns and e.get_namespace_uri(ns) or xsd_uri
|
|
if uri in (xsd_uri, soapenc_uri) and type_name != 'Array':
|
|
# look for the type, None == any
|
|
fn = REVERSE_TYPE_MAP.get(type_name, None)
|
|
elif uri == soapenc_uri and type_name == 'Array':
|
|
# arrays of simple types (look at the attribute tags):
|
|
fn = []
|
|
for a in e.children():
|
|
for k, v in a[:]:
|
|
if k.endswith(":arrayType"):
|
|
type_name = v
|
|
if ":" in type_name:
|
|
type_name = type_name[type_name.index(":")+1:]
|
|
if "[]" in type_name:
|
|
type_name = type_name[:type_name.index("[]")]
|
|
fn.append(REVERSE_TYPE_MAP.get(type_name, None))
|
|
else:
|
|
fn = None
|
|
|
|
if not fn:
|
|
# simple / complex type, postprocess later
|
|
if ns:
|
|
fn_namespace = uri # use the specified namespace
|
|
else:
|
|
fn_namespace = namespace # use parent namespace (default)
|
|
for k, v in e[:]:
|
|
if k.startswith("xmlns:"):
|
|
# get the namespace uri from the element
|
|
fn_namespace = v
|
|
fn = elements.setdefault(make_key(type_name, 'complexType', fn_namespace), OrderedDict())
|
|
|
|
if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'):
|
|
# it's an array... TODO: compound arrays? and check ns uri!
|
|
if isinstance(fn, OrderedDict):
|
|
if len(children) > 1 and dialect in ('jetty',):
|
|
# Jetty style support
|
|
# {'ClassName': [{'attr1': val1, 'attr2': val2}]
|
|
fn.array = True
|
|
else:
|
|
# .NET style support (backward compatibility)
|
|
# [{'ClassName': {'attr1': val1, 'attr2': val2}]
|
|
d.array = True
|
|
else:
|
|
if dialect in ('jetty',):
|
|
# scalar support [{'attr1': [val1]}]
|
|
fn = [fn]
|
|
else:
|
|
d.array = True
|
|
|
|
if (e['name'] is not None and not alias) or e['ref']:
|
|
e_name = e['name'] or type_name # for refs, use the type name
|
|
d[e_name] = fn
|
|
else:
|
|
log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name))
|
|
d[None] = fn
|
|
if e is not None and e.get_local_name() == 'extension' and e.children():
|
|
# extend base element:
|
|
process_element(elements, element_name, e.children(), element_type, xsd_uri, dialect, namespace, qualified)
|
|
elements.setdefault(make_key(element_name, element_type, namespace), OrderedDict()).update(d)
|
|
|
|
|
|
def postprocess_element(elements, processed):
|
|
"""Fix unresolved references (elements referenced before its definition, thanks .net)"""
|
|
|
|
# avoid already processed elements:
|
|
if elements in processed:
|
|
return
|
|
processed.append(elements)
|
|
|
|
for k, v in elements.items():
|
|
if isinstance(v, OrderedDict):
|
|
if v != elements: # TODO: fix recursive elements
|
|
postprocess_element(v, processed)
|
|
if None in v and v[None]: # extension base?
|
|
if isinstance(v[None], dict):
|
|
for i, kk in enumerate(v[None]):
|
|
# extend base -keep orginal order-
|
|
if v[None] is not None:
|
|
elements[k].insert(kk, v[None][kk], i)
|
|
del v[None]
|
|
else: # "alias", just replace
|
|
log.debug('Replacing %s = %s' % (k, v[None]))
|
|
elements[k] = v[None]
|
|
#break
|
|
if v.array:
|
|
elements[k] = [v] # convert arrays to python lists
|
|
if isinstance(v, list):
|
|
for n in v: # recurse list
|
|
if isinstance(n, (OrderedDict, list)):
|
|
#if n != elements: # TODO: fix recursive elements
|
|
postprocess_element(n, processed)
|
|
|
|
|
|
def get_message(messages, message_name, part_name):
|
|
if part_name:
|
|
# get the specific part of the message:
|
|
return messages.get((message_name, part_name))
|
|
else:
|
|
# get the first part for the specified message:
|
|
for (message_name_key, part_name_key), message in messages.items():
|
|
if message_name_key == message_name:
|
|
return message
|
|
|
|
|
|
get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s)
|
|
get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None)
|
|
|
|
|
|
def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces=None, qualified=False):
|
|
"""Find schema elements and complex types"""
|
|
|
|
from .simplexml import SimpleXMLElement # here to avoid recursive imports
|
|
|
|
# analyze the namespaces used in this schema
|
|
local_namespaces = {}
|
|
for k, v in schema[:]:
|
|
if k.startswith("xmlns"):
|
|
local_namespaces[get_local_name(k)] = v
|
|
if k == 'targetNamespace':
|
|
# URI namespace reference for this schema
|
|
if v == "urn:DefaultNamespace":
|
|
v = global_namespaces[None]
|
|
local_namespaces[None] = v
|
|
if k == 'elementFormDefault':
|
|
qualified = (v == "qualified")
|
|
# add schema namespaces to the global namespace dict = {URI: ns prefix}
|
|
for ns in local_namespaces.values():
|
|
if ns not in global_namespaces:
|
|
global_namespaces[ns] = 'ns%s' % len(global_namespaces)
|
|
|
|
for element in schema.children() or []:
|
|
if element.get_local_name() in ('import', 'include',):
|
|
schema_namespace = element['namespace']
|
|
schema_location = element['schemaLocation']
|
|
if schema_location is None:
|
|
log.debug('Schema location not provided for %s!' % schema_namespace)
|
|
continue
|
|
if schema_location in imported_schemas:
|
|
log.debug('Schema %s already imported!' % schema_location)
|
|
continue
|
|
imported_schemas[schema_location] = schema_namespace
|
|
log.debug('Importing schema %s from %s' % (schema_namespace, schema_location))
|
|
# Open uri and read xml:
|
|
xml = fetch(schema_location, http, cache, force_download, wsdl_basedir)
|
|
|
|
# Parse imported XML schema (recursively):
|
|
imported_schema = SimpleXMLElement(xml, namespace=xsd_uri)
|
|
preprocess_schema(imported_schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces, qualified)
|
|
|
|
element_type = element.get_local_name()
|
|
if element_type in ('element', 'complexType', "simpleType"):
|
|
namespace = local_namespaces[None] # get targetNamespace
|
|
element_ns = global_namespaces[ns] # get the prefix
|
|
element_name = element['name']
|
|
log.debug("Parsing Element %s: %s" % (element_type, element_name))
|
|
if element.get_local_name() == 'complexType':
|
|
children = element.children()
|
|
elif element.get_local_name() == 'simpleType':
|
|
children = element('restriction', ns=xsd_uri)
|
|
elif element.get_local_name() == 'element' and element['type']:
|
|
children = element
|
|
else:
|
|
children = element.children()
|
|
if children:
|
|
children = children.children()
|
|
elif element.get_local_name() == 'element':
|
|
children = element
|
|
if children:
|
|
process_element(elements, element_name, children, element_type, xsd_uri, dialect, namespace, qualified)
|
|
|
|
|
|
# simplexml utilities:
|
|
|
|
try:
|
|
_strptime = datetime.datetime.strptime
|
|
except AttributeError: # python2.4
|
|
_strptime = lambda s, fmt: datetime.datetime(*(time.strptime(s, fmt)[:6]))
|
|
|
|
|
|
# Functions to serialize/deserialize special immutable types:
|
|
def datetime_u(s):
|
|
fmt = "%Y-%m-%dT%H:%M:%S"
|
|
try:
|
|
return _strptime(s, fmt)
|
|
except ValueError:
|
|
try:
|
|
# strip utc offset
|
|
if s[-3] == ":" and s[-6] in (' ', '-', '+'):
|
|
warnings.warn('removing unsupported UTC offset', RuntimeWarning)
|
|
s = s[:-6]
|
|
# parse microseconds
|
|
try:
|
|
return _strptime(s, fmt + ".%f")
|
|
except:
|
|
return _strptime(s, fmt)
|
|
except ValueError:
|
|
# strip microseconds (not supported in this platform)
|
|
if "." in s:
|
|
warnings.warn('removing unsuppported microseconds', RuntimeWarning)
|
|
s = s[:s.index(".")]
|
|
return _strptime(s, fmt)
|
|
|
|
datetime_m = lambda dt: dt.isoformat()
|
|
date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date()
|
|
date_m = lambda d: d.strftime("%Y-%m-%d")
|
|
time_u = lambda s: _strptime(s, "%H:%M:%S").time()
|
|
time_m = lambda d: d.strftime("%H%M%S")
|
|
bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s]
|
|
bool_m = lambda s: {False: 'false', True: 'true'}[s]
|
|
|
|
|
|
# aliases:
|
|
class Alias(object):
|
|
def __init__(self, py_type, xml_type):
|
|
self.py_type, self.xml_type = py_type, xml_type
|
|
|
|
def __call__(self, value):
|
|
return self.py_type(value)
|
|
|
|
def __repr__(self):
|
|
return "<alias '%s' for '%s'>" % (self.xml_type, self.py_type)
|
|
|
|
if sys.version > '3':
|
|
long = Alias(int, 'long')
|
|
byte = Alias(str, 'byte')
|
|
short = Alias(int, 'short')
|
|
double = Alias(float, 'double')
|
|
integer = Alias(long, 'integer')
|
|
DateTime = datetime.datetime
|
|
Date = datetime.date
|
|
Time = datetime.time
|
|
|
|
# Define convertion function (python type): xml schema type
|
|
TYPE_MAP = {
|
|
unicode: 'string',
|
|
bool: 'boolean',
|
|
short: 'short',
|
|
byte: 'byte',
|
|
int: 'int',
|
|
long: 'long',
|
|
integer: 'integer',
|
|
float: 'float',
|
|
double: 'double',
|
|
Decimal: 'decimal',
|
|
datetime.datetime: 'dateTime',
|
|
datetime.date: 'date',
|
|
}
|
|
TYPE_MARSHAL_FN = {
|
|
datetime.datetime: datetime_m,
|
|
datetime.date: date_m,
|
|
bool: bool_m
|
|
}
|
|
TYPE_UNMARSHAL_FN = {
|
|
datetime.datetime: datetime_u,
|
|
datetime.date: date_u,
|
|
bool: bool_u,
|
|
str: unicode,
|
|
}
|
|
|
|
REVERSE_TYPE_MAP = dict([(v, k) for k, v in TYPE_MAP.items()])
|
|
|
|
REVERSE_TYPE_MAP.update({
|
|
'base64Binary': str,
|
|
})
|
|
|
|
# insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors)
|
|
if str not in TYPE_MAP:
|
|
TYPE_MAP[str] = 'string'
|
|
|
|
|
|
class OrderedDict(dict):
|
|
"""Minimal ordered dictionary for xsd:sequences"""
|
|
def __init__(self):
|
|
self.__keys = []
|
|
self.array = False
|
|
self.namespace = None
|
|
self.qualified = None
|
|
|
|
def __setitem__(self, key, value):
|
|
if key not in self.__keys:
|
|
self.__keys.append(key)
|
|
dict.__setitem__(self, key, value)
|
|
|
|
def insert(self, key, value, index=0):
|
|
if key not in self.__keys:
|
|
self.__keys.insert(index, key)
|
|
dict.__setitem__(self, key, value)
|
|
|
|
def __delitem__(self, key):
|
|
if key in self.__keys:
|
|
self.__keys.remove(key)
|
|
dict.__delitem__(self, key)
|
|
|
|
def __iter__(self):
|
|
return iter(self.__keys)
|
|
|
|
def keys(self):
|
|
return self.__keys
|
|
|
|
def items(self):
|
|
return [(key, self[key]) for key in self.__keys]
|
|
|
|
def update(self, other):
|
|
for k, v in other.items():
|
|
self[k] = v
|
|
# do not change if we are an array but the other is not:
|
|
if isinstance(other, OrderedDict) and not self.array:
|
|
self.array = other.array
|
|
if isinstance(other, OrderedDict) and not self.namespace:
|
|
self.namespace = other.namespace
|
|
self.qualified = other.qualified
|
|
|
|
def copy(self):
|
|
"Make a duplicate"
|
|
new = OrderedDict()
|
|
new.update(self)
|
|
return new
|
|
|
|
def __str__(self):
|
|
return "%s" % dict.__str__(self)
|
|
|
|
def __repr__(self):
|
|
s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()])
|
|
if self.array and False:
|
|
s = "[%s]" % s
|
|
return s
|