Files

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