2018年12月時点における、PythonのSOAPライブラリについて

Python その2 Advent Calendar 2018 - QiitaJSL (日本システム技研) Advent Calendar 2018 - Qiita の3日目の記事です。

 
APIというと、最近はRESTやgRPCなどがメジャーですが、場所によってはSOAPがまだまだ使われています。

 
もし、SOAPクライアント/サーバをPythonで実装しようと考えた時には、以下の記事で紹介されているライブラリを候補にあげるかもしれません。

 
ただ、最近はPython3系がメインになったこともあり、2018年12月時点で使えるPythonSOAPライブラリが気になりました。

調べてみると、SOAPライブラリの情報がPython Wikiにありました。
WebServices - Python Wiki

そこで今回は、Python Wikiに記載されているライブラリがPython3.7で動作するか試してみました。

 
目次

 

環境

 

また、今回使用するWSDLは以下とします。

returnMessage(<引数の値>) を呼んだら、 Hello, <引数の値> が返ってくる SOAP API となります。

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions
        xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
        xmlns:soap11="http://schemas.xmlsoap.org/wsdl/soap/"
        xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
        xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:ns0="http://example.com/HelloWorld"
        targetNamespace="http://example.com/HelloWorld">

    <wsdl:types>
        <xsd:schema elementFormDefault="qualified" targetNamespace="http://example.com/HelloWorld">
            <xsd:element name="RequestInterface">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element minOccurs="0" name="userName" type="xsd:string" />
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
            <xsd:element name="ResponseInterface">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element minOccurs="0" name="returnMessage" type="xsd:string" />
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
        </xsd:schema>
    </wsdl:types>

    <wsdl:message name="messageIn">
        <wsdl:part name="parameters" element="ns0:RequestInterface" />
    </wsdl:message>
    <wsdl:message name="messageOut">
        <wsdl:part name="parameters" element="ns0:ResponseInterface" />
    </wsdl:message>

    <wsdl:portType name="HelloPort">
        <wsdl:operation name="requestMessage">
            <wsdl:input message="ns0:messageIn"/>
            <wsdl:output message="ns0:messageOut"/>
        </wsdl:operation>
    </wsdl:portType>

    <wsdl:binding name="HelloBindingSoap11" type="ns0:HelloPort">
        <soap11:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <wsdl:operation name="requestMessage">
            <soap11:operation soapAction="http://example.com/HelloWorld/requestMessage" />
            <wsdl:input>
                <soap11:body use="literal"/>
            </wsdl:input>
            <wsdl:output>
                <soap11:body use="literal"/>
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>

    <wsdl:service name="HelloService">
        <wsdl:port name="HelloServicePort" binding="ns0:HelloBindingSoap11">
            <soap11:address location="http://localhost:9100/hello"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

 

長いのでまとめ

いろいろと書いていたら長くなったため、ひとまずまとめを書いておきます。

2018年12月時点で、新規にSOAPクライアント・サーバを実装する場合は、以下を使うのが良いのかなと感じました。

  • SOAPクライアント
    • Zeep
  • SOAPサーバ
    • Soapfish
    • Pythonライブラリではないが、テスト目的ならSOAP UIを使っても良いのでは

 

以降、それぞれのライブラリについて書いていきます。

 

SOAPクライアントライブラリ

Zeep

 
現在も活発にメンテナンスされ続けているライブラリです。Python2/3の両方で動作するとのことです。

また、公式ドキュメントが充実しているのもありがたいです。

 
Zeepですが、WSDLからSOAPクライアントを自動生成する機能はありません。

とはいえ、 -mzeep コマンドを使ってWSDLを解析できるため、実装の手がかりになります。

$ python -mzeep hello.wsdl 

Prefixes:
     xsd: http://www.w3.org/2001/XMLSchema
     ns0: http://example.com/HelloWorld

Global elements:
     ns0:RequestInterface(userName: xsd:string)
     ns0:ResponseInterface(returnMessage: xsd:string)
     

Global types:
     xsd:anyType
... (長いので省略)
     xsd:unsignedShort

Bindings:
     Soap11Binding: {http://example.com/HelloWorld}HelloBindingSoap11

Service: HelloService
     Port: HelloServicePort (Soap11Binding: {http://example.com/HelloWorld}HelloBindingSoap11)
         Operations:
            requestMessage(userName: xsd:string) -> returnMessage: xsd:string

 
Zeepの実装例として、WSDL中の requestMessage() に対する実装は以下の通りです。

import pathlib
from zeep import Client

# Zeepのスクリプトがあるディレクトリの親ディレクトリ上にWSDLを置いてある
WSDL = pathlib.Path(__file__).resolve().parents[1].joinpath('hello.wsdl')

client = Client(str(WSDL))
response = client.service.requestMessage(userName='taro')

print(response)
# => Hello, taro

 

suds系

2011年や2016年ではメインに使えていたsuds系ですが、元々の suds や、sudsをforkした suds-jurko はすでにメンテナスが停止しているようです。

 
最近メンテナンスされているのは、 suds-jurko をforkした suds-community のようです。
https://github.com/suds-community/suds

また、suds-communityのissueにはsudsのいろいろなforkが紹介されています。 https://github.com/suds-community/suds/issues/1

 
とはいえ、suds系はどのforkを使うのが一番良いのか分からなかったため、今回試すのはあきらめました。

   

SOAPサーバライブラリ

現時点では、既存のSOAPサーバを使うことはあるものの、ゼロからPythonSOAPサーバを作る機会はほとんどないと思います。

一方、既存のSOAP APIに対するテストサーバ構築のため、SOAPサーバライブラリを使うことがあるかもしれません。

簡単なSOAPテストサーバであれば、WSDLからモックを生成してくれる SOAP UI のCommunity版で十分かもしれません。 https://www.soapui.org/downloads/soapui.html

 
しかし今回は、SOAP UIでは足りない場合に備え、いくつかSOAPサーバライブラリを試していることにしました。

 

Soapfish

Python3.7系で動作するのに加え、WSDLから各種クラスを生成してくれる機能があるのが Soapfish です。

 
リポジトリが2つありますが、

  • FlightDataServicesからsoapteamがfork
  • 両者のmasterコミットは同じ
  • PyPI https://pypi.org/project/soapfish/ のHomePageからのリンク http://soapfish.org/ が soapteam のリポジトリにリダイレクト

を考えると、今後はsoapteamのほうがメンテナンスされていくのかもしれません(が、よく分かりません)。

 
そのSoapfishですが、ほとんどドキュメントがありません。唯一あるのがチュートリアルです。
https://github.com/soapteam/soapfish/blob/master/docs/tutorial.rst

ただ、今回扱う範囲ではこのチュートリアルで十分ですので、チュートリアルに従って試してみます。

 
インストールですが、PyPIでは提供されていないようです。

$ pip install soapfish
Collecting soapfish
  Could not find a version that satisfies the requirement soapfish (from versions: )
No matching distribution found for soapfish

 
そのため、soapteamのリポジトリからインストールします。

$ pip install git+https://github.com/soapteam/soapfish

 
次に、WSDLからSoapfish用のクラスを生成します。

そのままだとターミナル上に表示されるため、 soapfish_server/soap_server.py として保存します。

$ python -m soapfish.wsdl2py -s hello.wsdl > soapfish_server/soap_server.py

 
雛形ができましたので、これを元に修正を加えます。必要な箇所は3点です。

まずはOperationです。Hello, <引数の値> を返すように修正します。

# Operations

def requestMessage(request, RequestInterface):
    # TODO: Put your implementation here.
    # 自分の設定へと修正
    # return ResponseInterface
    return ResponseInterface(returnMessage=f'Hello, {RequestInterface.userName}')

 
次はSOAP Serviceです。 location の内容を修正します。

ただし、次のサーバ設定でエンドポイントを修正するため、適当な値で良さそうです。

# SOAP Service

HelloServicePort_SERVICE = soap.Service(
    name='HelloServicePort',
    targetNamespace='http://example.com/HelloWorld',

    # 自分の設定へと修正
    location='http://localhost/hello',
    # location='${scheme}://${host}/hello',

    schemas=[Schema_ab251],
    version=soap.SOAPVersion.SOAP11,
    methods=[requestMessage_method],
)

 
最後に、Soapfishを乗せるサーバの設定を行います。

雛形にはDjangoを使って動作させる方法がコメントで記載されています。

しかし、今回は標準モジュールの wsgiref で動かします。実装方法はチュートリアルに記載されています。
https://github.com/soapteam/soapfish/blob/master/docs/tutorial.rst#323-putting-it-all-together

 
ただ、1点注意するところは、チュートリアルのように

app = soap_dispatch.WsgiSoapApplication({'/ChargePoint/services/chargePointService': dispatcher})

とすると、 AttributeError: 'dict' object has no attribute 'dispatch' という例外が出て動作しません。

 
そのため、以下のように実装します。

from wsgiref.simple_server import make_server
from soapfish import soap_dispatch

dispatcher = soap_dispatch.SOAPDispatcher(HelloServicePort_SERVICE)

# 引数をdictからdispatcherへと修正(エラーが出たため)
# AttributeError: 'dict' object has no attribute 'dispatch'
app = soap_dispatch.WsgiSoapApplication(dispatcher)

print('Serving HTTP on port 9100...')
httpd = make_server('', 9100, app)
httpd.serve_forever()

 
あとは、Soapfishサーバを起動します。

$ python soap_server.py 
Serving HTTP on port 9100...

 
先ほど作成したZeepクライアントでアクセスすると、Hello, taro が返されるのが分かります。

 

Ladon

LadonSOAPサーバ用のPythonライブラリです。

Ladonに関する記事を探すと、過去には http://ladonize.org/ に公式サイトがあったようです。

しかし、現在は別のサービスがこのドメインを使用しているようで、そのURLにアクセスしても何も情報がありません。また、ドキュメントやサンプル類もいくつか失われているようです。

 
LadonによるSOAP APIの実装方法ですが、公式ドキュメントを読んでもどのように作ればよいのか、イマイチ分かりませんでした。そのため、今回はソースコードを読んで実装してみることにしました。

 
Soapfishと異なり、LadonではWSDLからSOAPサーバの雛形を生成する機能はないようです。そのため、自作していく必要があります。

また、Ladonの注意点として、デフォルトでは、SOAP APIの戻り値の名前が result に固定されています。

それは、ladon/src/ladon/interfaces/soap11.py にある SOAP11ResponseHandlerbuild_response() メソッドが原因です。 dict のkeyが result で固定されているためです。WSDLの定義と異なる場合、このままではうまく動作しません。

# https://bitbucket.org/jakobsg/ladon/src/42944fc012a3a48214791c120ee5619434505067/src/ladon/interfaces/soap11.py#lines-636
SOAP11ResponseHandler.value_to_soapxml(
    {'result': res_dict['result']}, method_elem, doc, is_toplevel=True)

 
対応方法ですが、ソースコードを読むと

# ladon/src/ladon/interfaces/__init__.py
# https://bitbucket.org/jakobsg/ladon/src/42944fc012a3a48214791c120ee5619434505067/src/ladon/interfaces/__init__.py#lines-29

This is a decorator for service interfaces. It basically checks that
the interface classes which are exposed meets the requirements for
interface implementations.
It adds the interface to a global interface-cache.
All default ladon interfaces are added using this decorator, if you are
extending Ladon with a new interface use this decorator.
Requirements:
------------
Interfaces must inherit the ladon.interfaces.base.BaseInterface class.

# ladon/src/ladon/interfaces/base.py
# https://bitbucket.org/jakobsg/ladon/src/42944fc012a3a48214791c120ee5619434505067/src/ladon/interfaces/base.py#lines-6

All interface implementations must descend from BaseInterface. The interface
implementation can be thought of as an aggregator that ties the three basic functions
of a web service protocol together:

    1. ServiceDescriptor - A generator class that can provide a description for the service.
       ie. WSDL for soap.
    2. BaseRequestHandler - A handler that can parse the raw request objects from the client and
       convert them into a so-called req_dict (Request dictionary)
    3. BaseResponseHandler - A handler that can process the res_dict (Result Dictionary) of a
       service call and build a response object that fit's the protocol being implemented.
    4. BaseFaultHandler - A handler that can convert a ladon.exceptions.service.ServiceFault object
       to a fault response that fits the interface.

より、

  • BaseResponseHandler を継承し、 build_response() を自分のレスポンス型に差し替えたResponseHandlerクラスを用意
  • @expose デコレータを付けた、BaseInterface を継承したInterfaceクラスを用意し、上記のResponseHandlerクラスに差し替え

とすれば良さそうです。

 
必要な部分を抜粋します(ソースコード全体は、こちら)

 
まずは、ResponseHandlerとなります。

class MySOAP11ResponseHandler(BaseResponseHandler):
    def build_response(self, res_dict, sinfo, encoding):
        ...
        if 'result' in res_dict['result']:
            SOAP11ResponseHandler.value_to_soapxml(
                res_dict['result'], method_elem, doc, is_toplevel=True)
        else:
            SOAP11ResponseHandler.value_to_soapxml(
                # 自分のレスポンス型へと修正
                {'returnMessage': res_dict['result']}, method_elem, doc, is_toplevel=True)
        body_elem.appendChild(method_elem)
        return doc.toxml(encoding=encoding)

 
次にInterfaceを実装します。

@expose
class MySOAP11Interface(BaseInterface):

    def __init__(self, sinfo, **kw):
        def_kw = {
            'service_descriptor': SOAP11ServiceDescriptor,
            'request_handler': SOAP11RequestHandler,
            
            # 自作のResponseHandlerへと修正
            'response_handler': MySOAP11ResponseHandler,
            'fault_handler': SOAP11FaultHandler}
        def_kw.update(kw)
        BaseInterface.__init__(self, sinfo, **def_kw)

    @staticmethod
    def _interface_name():
        # 差し替えたエンドポイントを新規作成する必要があるため、既存とは異なる値へと変更
        return 'mysoap11'
...

 
以上で、差し替えるためのResponseHandlerとInterfaceが完成しました。

 
続いて、SOAP用のサービスクラスを作成します。

サンプルのソースコードを読むと、WSGIアプリケーション化するLadonWSGIApplicationにて、ファイルパスを指定してサービスクラスを読み込んでいました。
https://bitbucket.org/jakobsg/ladon/src/42944fc012a3a48214791c120ee5619434505067/examples/handler.py#lines-16

そのため、今回もサービスクラスは別ファイルとして作成します。

サービスクラスを実装する際、 @ladonize デコレータを使う必要があります。

@ladonizeデコレータでは、

  • 位置引数にクライアントから受け取る引数の型
  • キーワード引数 rtype に、クライアントへ渡す型

をそれぞれ指定します。

class Hello(object):

    # メソッド名は、WSDLの
    # <wsdl:part name="parameters" element="ns0:RequestInterface" />
    # の定義名と合わせる
    @ladonize(str, rtype=str)
    def RequestInterface(self, userName):
        print(userName)
        return f'Hello, {userName}'

 
あとは、wsgirefを使ってサーバを作成します。

サーバは ladon/src/master/examples/runserver.py の書き方をそのまま流用します。
https://bitbucket.org/jakobsg/ladon/src/master/examples/runserver.py

scriptdir = dirname(abspath(__file__))
service_modules = ['hello', ]

# Create the WSGI Application
application = LadonWSGIApplication(
    service_modules,
    [join(scriptdir, 'hello'), ],
    catalog_name='Ladon Service Examples',
    catalog_desc='The services in this catalog serve as examples to how Ladon is used',
    logging=31)


port = 9100
print("\nExample services are running on port %(port)s.\nView browsable API at http://localhost:%(port)s\n" %
      {'port': port})

server = make_server('', port, application)
server.serve_forever()

 
これで実装が終わりました。

次にSOAPサーバを起動します。

$ python hello_server.py 

Example services are running on port 9100.
View browsable API at http://localhost:9100

 
注意点として、Ladon製SOAP APIのエンドポイントは http://localhost:9100/hello/mysoap11 です。そのため、WSDLの値とは異なってしまいます。

 
そのため、SOAPクライアント側でエンドポイントを差し替えることになります。

例えば、Zeepの場合は以下となります。

WSDL = pathlib.Path(__file__).resolve().parents[1].joinpath('hello.wsdl')

client = Client(str(WSDL))

# Clientオブジェクトのcreate_service()を使用し、ServiceProxyを生成
# ServiceProxyの中で、エンドポイントを差し替え
service = client.create_service(
    '{http://example.com/HelloWorld}HelloBindingSoap11',
    'http://localhost:9100/hello/mysoap11'
)

# ServiceProxyでSOAP APIと通信
response = service.requestMessage(userName='taro')

print(type(response))
# => <class 'str'>

print(response)
# => Hello, taro

 
サーバのログにも出力されました。

taro
127.0.0.1 - - "POST /hello/mysoap11 HTTP/1.1" 200 460

 
上記より、Python3.7でもLadonは動作しました。

ただ、Soapfishに比べて実装する部分が多く、またWSDLからの自動生成に対応していないため、手間がかかりました。

 

Python3.7では使うのが難しいSOAPサーバライブラリ

今回他にも試してみましたが、うまく動かなかったライブラリたちです。

Blogを書く時間の都合でこれらを難しいと判断しましたが、もし、対応方法をご存じの方がいらっしゃいましたら、教えて頂けるとありがたいです。

 

spyne

試しに、

class HelloWorldService(ServiceBase):
    @rpc(Unicode, _returns=Iterable(Unicode))
    def hello(ctx, name):
        return f'Hello, {name}'


application = Application([HelloWorldService],
                          tns='spyne.examples.hello',
                          in_protocol=Soap11(),
                          out_protocol=Soap11()
                          )


if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    wsgi_app = WsgiApplication(application)
    server = make_server('0.0.0.0', 9100, wsgi_app)
    server.serve_forever()

と実装し、起動させてみたところエラーとなりました。

  File "/path/to/lib/python3.7/site-packages/spyne/server/null.py", line 69
    self.service = _FunctionProxy(self, self.app, async=False)
                                                      ^
SyntaxError: invalid syntax

 
内部で使用しているNull Serverの実装で、Python3.5以降のキーワードとなった async が使われているようで、起動しませんでした。

そのため、Python3.7で使用するのはあきらめました。

 

pysimplesoap

こちらも試しに実装してみたところ、

Traceback (most recent call last):
File "python_soap_libraries-sample/env370/lib/python3.7/site-packages/pysimplesoap/server.py", line 163, in dispatch
ns = NS_RX.findall(xml)
TypeError: cannot use a string pattern on a bytes-like object

というエラーが発生しました。

WSGISOAPHandler が良くないのかと思い、bytes-like object を渡しているっぽいところを差し替えてみました。

class WSGISOAPPython3Handler(WSGISOAPHandler):
    def do_post(self, environ, start_response):
        length = int(environ['CONTENT_LENGTH'])
        request = environ['wsgi.input'].read(length)
        
        # request変数をstr()で文字列化
        response = self.dispatcher.dispatch(str(request))
        
        start_response('200 OK', [('Content-Type', 'text/xml'), ('Content-Length', str(len(response)))])
        return [response]

 
しかし、別のエラーが発生し、うまく動作しませんでした。

AttributeError: 'str' object has no attribute 'decode'

 
また、こことは関係ないかもしれませんが、pysimplesoapには動かないと報告されている issue も見つけました。

 
そのため、これ以上追いかけるのはあきらめました。

 
なお、pysimplesoapのドキュメントの一部が失われているようです。以下のissueからWebアーカイブをたどれます。
Where is the documentation? · Issue #123 · pysimplesoap/pysimplesoap

 

まとめ (再掲)

2018年12月時点で、新規にSOAPクライアント・サーバを実装する場合は、以下を使うのが良いのかなと感じました。

  • SOAPクライアント
    • Zeep
  • SOAPサーバ
    • Soapfish
    • Pythonライブラリではないが、テスト目的ならSOAP UIを使っても良いのでは

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/python_soap_libraries-sample

 

その他参考

最近のSOAPライブラリについては、以下でもまとめられていました。