Python その2 Advent Calendar 2018 - Qiita 兼 JSL (日本システム技研) Advent Calendar 2018 - Qiita の3日目の記事です。
APIというと、最近はRESTやgRPCなどがメジャーですが、場所によってはSOAPがまだまだ使われています。
もし、SOAPクライアント/サーバをPythonで実装しようと考えた時には、以下の記事で紹介されているライブラリを候補にあげるかもしれません。
ただ、最近は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"
<wsdldefinitions
xmlnswsdl="http://schemas.xmlsoap.org/wsdl/"
xmlnssoap11="http://schemas.xmlsoap.org/wsdl/soap/"
xmlnshttp="http://schemas.xmlsoap.org/wsdl/http/"
xmlnsmime="http://schemas.xmlsoap.org/wsdl/mime/"
xmlnsxsd="http://www.w3.org/2001/XMLSchema"
xmlnsns0="http://example.com/HelloWorld"
targetNamespace="http://example.com/HelloWorld">
<wsdltypes>
<xsdschema elementFormDefault="qualified" targetNamespace="http://example.com/HelloWorld">
<xsdelement name="RequestInterface">
<xsdcomplexType>
<xsdsequence>
<xsdelement minOccurs="0" name="userName" type="xsd:string" />
</xsdsequence>
</xsdcomplexType>
</xsdelement>
<xsdelement name="ResponseInterface">
<xsdcomplexType>
<xsdsequence>
<xsdelement minOccurs="0" name="returnMessage" type="xsd:string" />
</xsdsequence>
</xsdcomplexType>
</xsdelement>
</xsdschema>
</wsdltypes>
<wsdlmessage name="messageIn">
<wsdlpart name="parameters" element="ns0:RequestInterface" />
</wsdlmessage>
<wsdlmessage name="messageOut">
<wsdlpart name="parameters" element="ns0:ResponseInterface" />
</wsdlmessage>
<wsdlportType name="HelloPort">
<wsdloperation name="requestMessage">
<wsdlinput message="ns0:messageIn"/>
<wsdloutput message="ns0:messageOut"/>
</wsdloperation>
</wsdlportType>
<wsdlbinding name="HelloBindingSoap11" type="ns0:HelloPort">
<soap11binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<wsdloperation name="requestMessage">
<soap11operation soapAction="http://example.com/HelloWorld/requestMessage" />
<wsdlinput>
<soap11body use="literal"/>
</wsdlinput>
<wsdloutput>
<soap11body use="literal"/>
</wsdloutput>
</wsdloperation>
</wsdlbinding>
<wsdlservice name="HelloService">
<wsdlport name="HelloServicePort" binding="ns0:HelloBindingSoap11">
<soap11address location="http://localhost:9100/hello"/>
</wsdlport>
</wsdlservice>
</wsdldefinitions>
長いのでまとめ
いろいろと書いていたら長くなったため、ひとまずまとめを書いておきます。
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
WSDL = pathlib.Path(__file__).resolve().parents[1].joinpath('hello.wsdl')
client = Client(str(WSDL))
response = client.service.requestMessage(userName='taro')
print(response)
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サーバを使うことはあるものの、ゼロから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, <引数の値>
を返すように修正します。
def requestMessage(request, RequestInterface):
TODO
return ResponseInterface(returnMessage=f'Hello, {RequestInterface.userName}')
次はSOAP Serviceです。 location
の内容を修正します。
ただし、次のサーバ設定でエンドポイントを修正するため、適当な値で良さそうです。
HelloServicePort_SERVICE = soap.Service(
name='HelloServicePort',
targetNamespace='http://example.com/HelloWorld',
location='http://localhost/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)
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の定義と異なる場合、このままではうまく動作しません。
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,
'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):
@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', ]
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))
service = client.create_service(
'{http://example.com/HelloWorld}HelloBindingSoap11',
'http://localhost:9100/hello/mysoap11'
)
response = service.requestMessage(userName='taro')
print(type(response))
print(response)
サーバのログにも出力されました。
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)
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クライアント・サーバを実装する場合は、以下を使うのが良いのかなと感じました。
GitHubに上げました。
https://github.com/thinkAmi-sandbox/python_soap_libraries-sample
その他参考
最近のSOAPライブラリについては、以下でもまとめられていました。