Python その2 Advent Calendar 2018 - Qiita 兼 JSL (日本システム技研) Advent Calendar 2018 - Qiita の3日目の記事です。
APIというと、最近はRESTやgRPCなどがメジャーですが、場所によってはSOAPがまだまだ使われています。
もし、SOAPクライアント/サーバをPythonで実装しようと考えた時には、以下の記事で紹介されているライブラリを候補にあげるかもしれません。
- 2011年2月版
- 2016年12月版
ただ、最近はPython3系がメインになったこともあり、2018年12月時点で使えるPythonのSOAPライブラリが気になりました。
調べてみると、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
- リポジトリ
- 公式ドキュメント
現在も活発にメンテナンスされ続けているライブラリです。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
はすでにメンテナスが停止しているようです。
- https://bitbucket.org/jurko/suds/src
- https://bitbucket.org/jurko/suds/issues/117/please-release-new-version#comment-35284706
最近メンテナンスされているのは、 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サーバを使うことはあるものの、ゼロからPythonでSOAPサーバを作る機会はほとんどないと思います。
一方、既存の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
Ladon
もSOAPサーバ用のPythonライブラリです。
- リポジトリ
- 公式ドキュメント
Ladonに関する記事を探すと、過去には http://ladonize.org/
に公式サイトがあったようです。
しかし、現在は別のサービスがこのドメインを使用しているようで、そのURLにアクセスしても何も情報がありません。また、ドキュメントやサンプル類もいくつか失われているようです。
LadonによるSOAP APIの実装方法ですが、公式ドキュメントを読んでもどのように作ればよいのか、イマイチ分かりませんでした。そのため、今回はソースコードを読んで実装してみることにしました。
Soapfishと異なり、LadonではWSDLからSOAPサーバの雛形を生成する機能はないようです。そのため、自作していく必要があります。
また、Ladonの注意点として、デフォルトでは、SOAP APIの戻り値の名前が result
に固定されています。
それは、ladon/src/ladon/interfaces/soap11.py
にある SOAP11ResponseHandler
の build_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 も見つけました。
- https://github.com/pysimplesoap/pysimplesoap/issues/167
- https://github.com/pysimplesoap/pysimplesoap/issues/171
そのため、これ以上追いかけるのはあきらめました。
なお、pysimplesoapのドキュメントの一部が失われているようです。以下のissueからWebアーカイブをたどれます。
Where is the documentation? · Issue #123 · pysimplesoap/pysimplesoap
まとめ (再掲)
2018年12月時点で、新規にSOAPクライアント・サーバを実装する場合は、以下を使うのが良いのかなと感じました。
ソースコード
GitHubに上げました。
https://github.com/thinkAmi-sandbox/python_soap_libraries-sample
その他参考
最近のSOAPライブラリについては、以下でもまとめられていました。