From 0125de0a79d99e6dcd9f7b90ea7a67718ba367eb Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Fri, 23 Sep 2011 17:48:33 +0200 Subject: [PATCH] Making progress on the soap implementation. Will have to choose between Genshi and ElementTree though, it is getting anoying to juggle with both --- wsme/controller.py | 22 +++++--- wsme/exc.py | 3 ++ wsme/rest.py | 4 +- wsme/restjson.py | 2 +- wsme/restxml.py | 2 +- wsme/soap.py | 95 +++++++++++++++++++++++++++++------ wsme/templates/soap.html | 2 +- wsme/tests/test_controller.py | 6 +-- wsme/tests/test_soap.py | 53 ++++++++++++++----- 9 files changed, 146 insertions(+), 43 deletions(-) diff --git a/wsme/controller.py b/wsme/controller.py index e53f34a..609d874 100644 --- a/wsme/controller.py +++ b/wsme/controller.py @@ -132,6 +132,7 @@ class WSRoot(object): if isinstance(protocol, str): protocol = registered_protocols[protocol]() self.protocols[protocol.name] = protocol + protocol.root = weakref.proxy(self) self._api = None @@ -146,7 +147,7 @@ class WSRoot(object): protocol = self.protocols[request.params['wsmeproto']] else: for p in self.protocols.values(): - if p.accept(self, request): + if p.accept(request): protocol = p break return protocol @@ -164,11 +165,12 @@ class WSRoot(object): log.error(msg) return res path = protocol.extract_path(request) + if path is None: + raise exc.ClientSideError( + u'The %s protocol was unable to extract a function ' + u'path from the request' % protocol.name) func, funcdef = self._lookup_function(path) - kw = protocol.read_arguments(request, funcdef) - - if funcdef.protocol_specific: - kw['root'] = self + kw = protocol.read_arguments(funcdef, request) result = func(**kw) @@ -178,12 +180,15 @@ class WSRoot(object): res.body = result else: # TODO make sure result type == a._wsme_definition.return_type - res.body = protocol.encode_result(result, funcdef) + res.body = protocol.encode_result(funcdef, result) res_content_type = funcdef.contenttype except Exception, e: infos = self._format_exception(sys.exc_info()) log.error(str(infos)) - res.status = 500 + if isinstance(e, exc.ClientSideError): + res.status = 400 + else: + res.status = 500 res.body = protocol.encode_error(infos) if res_content_type is None: @@ -237,6 +242,7 @@ class WSRoot(object): r = dict(faultcode="Client", faultstring=unicode(excinfo[1])) log.warning("Client-side error: %s" % r['faultstring']) + r['debuginfo'] = None return r else: faultstring = str(excinfo[1]) @@ -248,6 +254,8 @@ class WSRoot(object): r = dict(faultcode="Server", faultstring=faultstring) if self._debug: r['debuginfo'] = debuginfo + else: + r['debuginfo'] = None return r def _html_format(self, content, content_types): diff --git a/wsme/exc.py b/wsme/exc.py index 6a3fda4..033d30b 100644 --- a/wsme/exc.py +++ b/wsme/exc.py @@ -5,6 +5,9 @@ if '_' not in __builtin__.__dict__: class ClientSideError(RuntimeError): + def __unicode__(self): + return RuntimeError.__str__(self) + def __str__(self): return unicode(self).encode('utf8', 'ignore') diff --git a/wsme/rest.py b/wsme/rest.py index 19a0a9c..f86c9d5 100644 --- a/wsme/rest.py +++ b/wsme/rest.py @@ -10,12 +10,12 @@ class RestProtocol(object): dataformat = None content_types = [] - def accept(self, root, request): + def accept(self, request): if request.path.endswith('.' + self.dataformat): return True return request.headers.get('Content-Type') in self.content_types - def read_arguments(self, request, funcdef): + def read_arguments(self, funcdef, request): if len(request.params) and request.body: raise ClientSideError( "Cannot read parameters from both a body and GET/POST params") diff --git a/wsme/restjson.py b/wsme/restjson.py index 8d3463c..3a40e3d 100644 --- a/wsme/restjson.py +++ b/wsme/restjson.py @@ -106,7 +106,7 @@ class RestJsonProtocol(RestProtocol): raw_args = json.loads(body) return raw_args - def encode_result(self, result, funcdef): + def encode_result(self, funcdef, result): r = tojson(funcdef.return_type, result) return json.dumps({'result': r}, ensure_ascii=False).encode('utf8') diff --git a/wsme/restxml.py b/wsme/restxml.py index 2c77474..108c7c2 100644 --- a/wsme/restxml.py +++ b/wsme/restxml.py @@ -125,7 +125,7 @@ class RestXmlProtocol(RestProtocol): def parse_args(self, body): return dict((sub.tag, sub) for sub in et.fromstring(body)) - def encode_result(self, result, funcdef): + def encode_result(self, funcdef, result): return et.tostring(toxml(funcdef.return_type, 'result', result)) def encode_error(self, errordetail): diff --git a/wsme/soap.py b/wsme/soap.py index b8e6660..6c97f59 100644 --- a/wsme/soap.py +++ b/wsme/soap.py @@ -1,21 +1,63 @@ +""" +A SOAP implementation for wsme. +Parts of the code were taken from the tgwebservices soap implmentation. +""" + import pkg_resources +import datetime +import decimal + +from simplegeneric import generic from xml.etree import ElementTree as et from genshi.template import MarkupTemplate from wsme.controller import register_protocol, pexpose import wsme.types -nativetypes = { +type_registry = { + basestring: 'xsd:string', str: 'xsd:string', int: 'xsd:int', + long: "xsd:long", + float: "xsd:float", + bool: "xsd:boolean", + #unsigned: "xsd:unsignedInt", + datetime.datetime: "xsd:dateTime", + datetime.date: "xsd:date", + datetime.time: "xsd:time", + decimal.Decimal: "xsd:decimal", + wsme.types.binary: "xsd:base64Binary", } + +def make_soap_element(datatype, tag, value): + el = et.Element(tag) + if value is None: + el.set('xsi:nil', 'true') + else: + el.set('xsi:type', type_registry.get(datatype)) + el.text = str(value) + return el + + +@generic +def tosoap(datatype, tag, value): + """Converts a value into xml Element objects for inclusion in the SOAP + response output""" + return make_soap_element(datatype, tag, value) + +@tosoap.when_object(None) +def None_tosoap(datatype, tag, value): + return make_soap_element(datatype, tag, None) + class SoapProtocol(object): name = 'SOAP' content_types = ['application/soap+xml'] ns = { - "soap": "http://www.w3.org/2001/12/soap-envelope" + "soap": "http://www.w3.org/2001/12/soap-envelope", + "soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "soapenc": "http://schemas.xmlsoap.org/soap/encoding/", } def __init__(self, tns=None, @@ -25,8 +67,19 @@ class SoapProtocol(object): self.tns = tns self.typenamespace = typenamespace self.servicename = 'MyApp' + self._name_mapping = {} - def accept(self, root, req): + def get_name_mapping(self, service=None): + if service not in self._name_mapping: + self._name_mapping[service] = dict( + (self.soap_fname(f), f.path + [f.name]) + for f in self.root.getapi() if service is None or (f.path and f.path[0] == service) + ) + print self._name_mapping + return self._name_mapping[service] + + + def accept(self, req): if req.path.endswith('.wsdl'): return True for ct in self.content_types: @@ -36,20 +89,30 @@ class SoapProtocol(object): def extract_path(self, request): if request.path.endswith('.wsdl'): - print "Here !!" return ['_protocol', self.name, 'api_wsdl'] el = et.fromstring(request.body) - body = el.find('{http://schemas.xmlsoap.org/soap/envelope/}Body') + body = el.find('{%(soapenv)s}Body' % self.ns) + # Extract the service name from the tns fname = list(body)[0].tag - print fname - return [fname] + if fname.startswith('{%s}' % self.tns): + fname = fname[len(self.tns)+2:] + return self.get_name_mapping()[fname] + return None - def read_arguments(self, request, funcdef): + def read_arguments(self, funcdef, request): return {} - def encode_result(self, result, funcdef): - envelope = self.render_template('soap') - print envelope + def soap_response(self, funcdef, result): + r = et.Element('{' + self.tns + '}' + self.soap_fname(funcdef) + 'Response') + r.append(tosoap(funcdef.return_type, 'result', result)) + return et.tostring(r) + + def encode_result(self, funcdef, result): + envelope = self.render_template('soap', + typenamespace=self.typenamespace, + result=result, + funcdef=funcdef, + soap_response=self.soap_response) return envelope def get_template(self, name): @@ -67,7 +130,7 @@ class SoapProtocol(object): **infos) @pexpose(contenttype="text/xml") - def api_wsdl(self, root, service=None): + def api_wsdl(self, service=None): if service is None: servicename = self.servicename else: @@ -75,10 +138,10 @@ class SoapProtocol(object): return self.render_template('wsdl', tns = self.tns, typenamespace = self.typenamespace, - soapenc = 'http://schemas.xmlsoap.org/soap/encoding/', + soapenc = self.ns['soapenc'], service_name = servicename, complex_types = (t() for t in wsme.types.complex_types), - funclist = root.getapi(), + funclist = self.root.getapi(), arrays = [], list_attributes = wsme.types.list_attributes, baseURL = service, @@ -87,8 +150,8 @@ class SoapProtocol(object): ) def soap_type(self, datatype): - if datatype in nativetypes: - return nativetypes[datatype] + if datatype in type_registry: + return type_registry[datatype] if wsme.types.iscomplex(datatype): return "types:%s" % datatype.__name__ diff --git a/wsme/templates/soap.html b/wsme/templates/soap.html index d4114ed..113f4c4 100644 --- a/wsme/templates/soap.html +++ b/wsme/templates/soap.html @@ -5,6 +5,6 @@ xmlns:py="http://genshi.edgewall.org/" py:attrs="{'xmlns' : typenamespace}"> - ${soap_body(methodname, function_info, output)} + ${Markup(soap_response(funcdef, result))} diff --git a/wsme/tests/test_controller.py b/wsme/tests/test_controller.py index c8101fc..7ad8719 100644 --- a/wsme/tests/test_controller.py +++ b/wsme/tests/test_controller.py @@ -14,18 +14,18 @@ class DummyProtocol(object): def __init__(self): self.hits = 0 - def accept(self, root, req): + def accept(self, req): return True def extract_path(self, request): return ['touch'] - def read_arguments(self, request, arguments): + def read_arguments(self, funcdef, request): self.lastreq = request self.hits += 1 return {} - def encode_result(self, result, return_type): + def encode_result(self, funcdef, result): return str(result) def encode_error(self, infos): diff --git a/wsme/tests/test_soap.py b/wsme/tests/test_soap.py index 6204c16..ba66743 100644 --- a/wsme/tests/test_soap.py +++ b/wsme/tests/test_soap.py @@ -10,6 +10,10 @@ except: import wsme.soap +tns = "http://foo.bar.baz/soap/" + +soapenv_ns = 'http://schemas.xmlsoap.org/soap/envelope/' +body_qn = '{%s}Body' % soapenv_ns def build_soap_message(method, params=""): message = """ @@ -18,14 +22,14 @@ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> - + <%(method)s> %(params)s -""" % dict(method=method, params=params) +""" % dict(method=method, params=params, tns=tns) return message @@ -53,8 +57,19 @@ def loadxml(el): return el.text +soap_types = { + 'xsi:int': int +} + +def fromsoap(el): + t = el.get('type') + if t in soap_types: + return soap_types[t](el.text) + return None + + class TestSOAP(wsme.tests.protocol.ProtocolTestCase): - protocol = 'SOAP' + protocol = wsme.soap.SoapProtocol(tns=tns) def test_simple_call(self): message = build_soap_message('Touch') @@ -67,19 +82,33 @@ class TestSOAP(wsme.tests.protocol.ProtocolTestCase): assert res.status.startswith('200') def call(self, fpath, **kw): + path = fpath.strip('/').split('/') # get the actual definition so we can build the adequate request - - el = dumpxml('parameters', kw) - content = et.tostring(el) - res = self.app.post( - '/' + fpath, - content, + params = "" + methodname = ''.join((i.capitalize() for i in path)) + message = build_soap_message(methodname, params) + res = self.app.post('/', message, headers={ - 'Content-Type': 'text/xml', - }, - expect_errors=True) + "Content-Type": "application/soap+xml; charset=utf-8" + }, expect_errors=True) print "Received:", res.body + el = et.fromstring(res.body) + body = el.find(body_qn) + print body + + if res.status_int == 200: + r = body.find('{%s}%sResponse' % (tns, methodname)) + result = r.find('{%s}result' % tns) + return fromsoap(result) + elif res.status_int == 400: + pass + elif res.status_int == 500: + pass + + + + if el.tag == 'error': raise wsme.tests.protocol.CallException( el.find('faultcode').text,