Python + openpyxlで、ブックやシートの保護・解除を試してみた

Excelには、ブックやシートを保護するための機能があります。

  • 読み取りパスワード
  • 書き込みパスワード
  • ブックの保護
  • シートの保護

 
それらをopnepyxlでやるにはどうしたら良いかを試した時のメモです。

なお、パスワードは把握している前提です。パスワードのクラックではありません。

また、公式ドキュメントだとこのあたりのことです。
Protection — openpyxl 2.5.12 documentation

 
目次

 

環境

 

読み取りパスワード・書き込みパスワードの設定は不可

openpyxlでは読み取りパスワードと書き込みパスワードは設定できないようです。

issueによると

because it is proprietary MS code not covered by the OOXML specification.

https://bitbucket.org/openpyxl/openpyxl/issues/193/can-i-open-password-protected-excel-files

とのことです。

 

ブックの保護

パスワード無しでブックの保護を行う

Workbookオブジェクトには security 属性があり、それに対して保護の設定を行うことで実現できます。

ブックの保護なしのファイルを読み込むと、 security 属性は None です。

そのため、 openpyxl.workbook.protection.WorkbookProtection オブジェクトを設定した上で、 lockStructure = True とします。

# ブックを読み込み
wb = _load(NO_PROTECTION_FILE)

# ブックを保護
wb.security = WorkbookProtection()
wb.security.lockStructure = True

# 保存
_save(wb, f'No_1_PROTECT_book_using_{NO_PROTECTION_FILE}')


# _load()と_save()は以下の関数(以降のソースコードも同様)
# ディレクトリ名を付けると一行が長くなって見づらかったので、関数化した
def _load(file_name):
    return openpyxl.load_workbook(BASE_FILE_DIR.joinpath(file_name))

def _save(workbook, file_name):
    workbook.save(RESULT_FILE_DIR.joinpath(file_name))

 
結果です。

f:id:thinkAmi:20181215100903p:plain:w300

 

パスワードありでブックを保護する

パスワード無しに加え、 wb.security.workbook_password = PASSWORD_FOR_BOOK を追加します。

wb = _load(NO_PROTECTION_FILE)

# ブックを保護
wb.security = WorkbookProtection()
wb.security.lockStructure = True
wb.security.workbook_password = PASSWORD_FOR_BOOK

# 保存
_save(wb, f'No_2_PROTECT_book_using_{NO_PROTECTION_FILE}')

 
結果です。ブックの保護を解除しようとすると、パスワード入力が求められます。

f:id:thinkAmi:20181215101129p:plain:w300

 

シートの保護

パスワード無しでシートを保護する

シートオブジェクトの protection 属性にある enable() メソッドを使います。

なお、公式ドキュメントではブックオブジェクトに対して設定するよう記載されていましたが、シートオブジェクトが正しそうです。
https://openpyxl.readthedocs.io/en/stable/protection.html#worksheet-protection

wb = _load(NO_PROTECTION_FILE)

# 対象のワークシートオブジェクトを取得する
ws = wb['Sheet1']

# パスワード無しで保護
ws.protection.enable()

# 保存
_save(wb, f'No_3_PROTECT_sheet_without_password_{NO_PROTECTION_FILE}')

 
結果です。

f:id:thinkAmi:20181215101422p:plain:w300

 

パスワードありでシートを保護する

パスワードなしに加え、 ws.protection.password = PASSWORD_FOR_SHEET を使います。

# 対象のワークシートオブジェクトを取得する
ws = wb['Sheet1']

# パスワードをセット
ws.protection.password = PASSWORD_FOR_SHEET

# シートを保護
ws.protection.enable()

 

シートの保護時に「このシートのすべてのユーザーに許可する操作」を設定する

Excelでは、シートの保護時に このシートのすべてのユーザーに許可する操作 というチェックボックスがいくつかあります。

openpyxlでそれらを有効化する場合は以下となります。

# 対象のワークシートオブジェクトを取得する
ws = wb['Sheet1']

# このシートのすべてのユーザーに許可する操作
ws.protection.objects = True                # オブジェクトの編集
ws.protection.scenarios = True              # シナリオの編集
ws.protection.formatCells = True            # セルの書式設定
ws.protection.formatColumns = True          # 列の書式設定
ws.protection.formatRows = True             # 行の書式設定
ws.protection.insertColumns = True          # 列の挿入
ws.protection.insertRows = True             # 行の挿入
ws.protection.insertHyperlinks = True       # ハイパーリンクの挿入
ws.protection.deleteColumns = True          # 列の削除
ws.protection.deleteRows = True             # 行の削除
ws.protection.selectLockedCells = True      # ロックされたセルの選択
ws.protection.selectUnlockedCells = True    # ロックされていないセルの選択
ws.protection.sort = True                   # 並べ替え
ws.protection.autoFilter = True             # フィルター
ws.protection.pivotTables = True            # ピボットテーブルレポート

# パスワード無しで保護
ws.protection.enable()

 

セルをロックしないで、シートを保護する

Excelのデフォルトでは、シートを保護すると全セルにロックがかかり、セルへの入力ができなくなります。

一部のセルのみ入力可能にしてシートを保護したい場合、事前にセルのロックを解除します。

openpyxlでは、以下のようにします。

from openpyxl.styles import Protection

wb = _load(NO_PROTECTION_FILE)
ws = wb['Sheet1']

# ロックを外したい(保護されない)セルを選ぶ
unlock_cells = ws['A1:B3']

# 取得したデータや型を見ると、行ごとにタプルでセルが入っている
print(f'type: ({type(unlock_cells)}), values: {unlock_cells}')
# => type: (<class 'tuple'>), values: ((<Cell 'Sheet1'.A1>, <Cell 'Sheet1'.B1>),
#                                      (<Cell 'Sheet1'.A2>, <Cell 'Sheet1'.B2>),
#                                      (<Cell 'Sheet1'.A3>, <Cell 'Sheet1'.B3>))

# chain.from_iterable()でネストタプルを平坦にしてから処理 (使ってみたかっただけ)
# 普通は for の2重ループで良いのかな
for cell in chain.from_iterable(unlock_cells):

    # 念のための確認
    print(f'type: ({type(cell)}), values: {cell}')
    # => type: (<class 'openpyxl.cell.cell.Cell'>), values: <Cell 'Sheet1'.A1>

    # ロックを解除
    cell.protection = Protection(locked=False)

# シートを保護
ws.protection.enable()

 
結果です。

ロックされたセルの場合、入力しようとすると、以下のようにメッセージが表示されます。

f:id:thinkAmi:20181215102139p:plain:w300

一方、ロックされていないセルの場合、入力が可能です。

f:id:thinkAmi:20181215102154p:plain:w300

 

ブックの保護を解除

ブックの保護時とは逆で、 wb.security.lockStructure = False とします。

また、パスワード付きブックの保護の場合は、 wb.security.workbook_password = PASSWORD_FOR_BOOK を設定します。

wb = _load(BOOK_PROTECTION_FILE)

# 保護したときのパスワードをセット
wb.security.workbook_password = PASSWORD_FOR_BOOK

# ブックの保護を解除
# wb.security.lock_structureでも良い:Aliasが設定されている
wb.security.lockStructure = False

_save(wb, f'No_6_UNPROTECT_{BOOK_PROTECTION_FILE}')

 
結果です。ブックの保護が解除されています。

f:id:thinkAmi:20181215102630p:plain:w300

 

シートの保護を解除

シートの保護とは別のメソッド ws.protection.disable() を使います。

また、パスワードで保護している場合は、 ws.protection.password = PASSWORD_FOR_SHEET で設定する必要があります。

wb = _load(SHEET_PROTECTION_WITH_PASSWORD_FILE)
ws = wb['Sheet1']

# シートを保護したときのパスワードをセット
ws.protection.password = PASSWORD_FOR_SHEET

# シートの保護を解除
ws.protection.disable()

_save(wb, f'No_8_UNPROTECT_{SHEET_PROTECTION_WITH_PASSWORD_FILE}')

 
結果です。指定したシート Sheet1 のみシートの保護が解除されています。

f:id:thinkAmi:20181215103043p:plain:w300

 

ソースコード

GitHubに上げました。openpyxl/protection/ ディレクトリの中が今回のファイルです。
https://github.com/thinkAmi-sandbox/python_excel_libraries-sample

Python + openpyxlで、 0:00 という値を持つセルが正しく保存されない

JSL (日本システム技研) Advent Calendar 2018 - Qiita の5日目の記事です。

 
Excel (*.xlsx) ファイルをPythonで作成するため、openpyxlを使いました。
https://bitbucket.org/openpyxl/openpyxl/src

 
そんな中、 0:00 という値を持つセルが正しく保存されなかったため、原因調査と対応を行ったときのメモです。

 
目次

 

環境

 

現象

Excel上で A1 セルに 0:00 、A2セルに 0:01 を入力したシートを用意します。

f:id:thinkAmi:20181205212715p:plain:w450

 
表示形式は以下の通りです。

f:id:thinkAmi:20181205212735p:plain:w450

 
このようなファイルに対し、openpyxlで開いて別名で保存をします。

BEFORE = "zero_time.xlsx"
AFTER = "zero_time_after.xlsx"

wb = openpyxl.load_workbook(BEFORE)
wb.save(AFTER)

 
別名で保存したファイルを開くと、 0:00 を設定したセルが変更されてしまっています。

f:id:thinkAmi:20181205212913p:plain

 

原因

openpyxlのissueを見ると、似たような事例がありました。
openpyxl / openpyxl / issues / #1043 - Time '0:00' is not saved correctly after loading and saving a file — Bitbucket

いわゆる1900年うるう日問題が原因のようです。

 
どこかでこのあたりを見たなと思ったら、過去に書いていました。
C#とAccessにおける、日付型(DateTime)のデフォルト値の差について - メモ的な思考的な

 
では、実際に値を見てみます。

def print_before_and_after(before, after):
    print('--- before ---')
    print_a1_a2(before)

    print('--- after ---')
    print_a1_a2(after)


def print_a1_a2(path):
    wb = openpyxl.load_workbook(path)
    sheet = wb.active
    a1_value = sheet['A1'].value
    a2_value = sheet['A2'].value
    print(f'A1: {a1_value}({type(a1_value)}), A2: {a2_value}({type(a2_value)})')
    wb.close()


print_before_and_after(BEFORE, AFTER)
# =>
# --- before ---
# A1: 1899-12-30 00:00:00(<class 'datetime.datetime'>), A2: 00:01:00(<class 'datetime.time'>)
# --- after ---
# A1: 1899-12-29 00:00:00(<class 'datetime.datetime'>), A2: 00:01:00(<class 'datetime.time'>)

 
気になる点は、

  • 1899/12/30から1899/12/29へと値が変更されていた
  • 0:00は datetime.datetime 型、0:01は datetime.date 型と、違いがある

です。

Excel上でも -1 と数式欄に表示されていたのも、この影響のように見えます。

 

対応

【NG】iso_dates=True を設定

まずは、issueに記載されていたように、 workbook.iso_dates=True を設定してみます。

wb = openpyxl.load_workbook(BEFORE)
wb.iso_dates = True
wb.save(ISO)

print_before_and_after(BEFORE, ISO)
# =>
# --- before ---
# A1: 1899-12-30 00:00:00(<class 'datetime.datetime'>), A2: 00:01:00(<class 'datetime.time'>)
# --- after ---
# A1: 1899-12-30 00:00:00(<class 'datetime.datetime'>), A2: 00:01:00(<class 'datetime.time'>)

データ上は良さそうです。

ただ、実際のファイルを開いてみたところ、 #VALUE となっていました。

f:id:thinkAmi:20181205214224p:plain:w450

 

Excelのゼロ日付 (1899/12/30) の時だけ特別処理

0:00 の時は日付が -1 されてしまうため、ゼロ日付の時は日付を1加算するロジックを追加してみました。

FIXED = "zero_time_fix.xlsx"
EXCEL_ZERO_DATE = datetime(1899, 12, 30, 0, 0, 0)

wb = openpyxl.load_workbook(BEFORE)
wb = fix_default_date(wb, "A1")
wb = fix_default_date(wb, "A2")
wb.save(FIXED)


def fix_default_date(workbook, cell):
    sheet = workbook.active
    value = sheet[cell].value
    if value == EXCEL_DEFAULT_DATE:
        sheet[cell].value = EXCEL_ZERO_DATE + timedelta(days=1)
    return workbook

 
データを確認してみましたが、大丈夫そうです。

print_before_and_after(BEFORE, FIXED)
# =>
# --- before ---
# A1: 1899-12-30 00:00:00(<class 'datetime.datetime'>), A2: 00:01:00(<class 'datetime.time'>)
# --- after ---
# A1: 1899-12-30 00:00:00(<class 'datetime.datetime'>), A2: 00:01:00(<class 'datetime.time'>)

 
表示上も良さそうでした。

f:id:thinkAmi:20181205214746p:plain

 

ソースコード

GitHubに上げました。 openpyxl/zero_time ディレクトリが今回のものです。
https://github.com/thinkAmi-sandbox/python_excel_libraries-sample

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ライブラリについては、以下でもまとめられていました。

Python + Zeep で、WSDLで定義された SOAP API エンドポイントとは別のエンドポイントにアクセスする

SOAP APIのテストなどでは、

  • WSDLの定義とは別のエンドポイント(テスト用エンドポイント)にアクセスしたい
  • ただし、WSDLは修正したくない

という状況があるかもしれません。

 
そこで、Zeepではどのようにやるのかを調べた時のメモを残します。

 

目次

 

環境

 

方法

公式ドキュメントに記載がありました。 ServiceProxy オブジェクトを使えば良いようです。
Creating new ServiceProxy objects | The Client object — Zeep 3.1.0 documentation

 

検証

実際に試してみます。

 

SOAP APIの仕様

以下のようなBindingのWSDLを持つSOAP APIがあるとします。

項目
名前空間 http://example.com/HelloWorld
エンドポイント http://localhost:8088/mockHelloBindingSoap11
メソッド requestMessage()
メソッドの挙動 引数に値を渡してリクエストすると、 Hello, <引数の値> がレスポンスされる

 
実際のWSDL (Hello.wsdl) はこちら。

<?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:/example.com/hello"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

 
ただ、本番のエンドポイント http:/example.com/hello が現在稼働していないため、そのままではテストできない状況とします。

 
そこで、 http:/example.com/hello ではなく、ローカルのポートにアクセスさせることにより、テストを実施させるとします。

また、その時のローカルでは、ポート 9100 にてSOAP APIがLISTENしているとします。

$ sudo lsof -i -P | grep "LISTEN"

...
... TCP *:9100 (LISTEN)

 

実装

ZeepでSOAPクライアントを作るところは、今までと同じです。

import pathlib

from zeep import Client

WSDL = pathlib.Path(__file__).parents[0].joinpath('Hello.wsdl')

client = Client(str(WSDL))

 
次に、Clientオブジェクトの create_service() を使い、Service Proxyを生成します。

create_service() の引数は、それぞれ以下となります。

  • 第一引数は、差し替え元の名前空間付きBinding名
  • 第二引数は、差し替え後のエンドポイント
service = client.create_service(
    '{http://example.com/HelloWorld}HelloBindingSoap11',
    'http://localhost:9100/hello'
)

 
あとは Service Proxyオブジェクトを使い、 requestMessage() を呼び出します。

response = service.requestMessage(userName='taro')

print(type(response))
print(response)

 
このスクリプトを実行すると、

<class 'str'>
Hello, taro

と、エンドポイントが差し替えられたことが分かりました。

 

ソースコード

GitHubに上げました。 chanbge_endpoint ディレクトリが今回のものです。
https://github.com/thinkAmi-sandbox/python_zeep-sample

Python + Zeep + History pluginで、SOAPのリクエストとレスポンスを確認してみた

前回、Zeep + SOAP UI + 自作WSDLを使ってSOAPのリクエスト・レスポンスを試してみました。
Python + Zeep + SOAP UI + 自作WSDLで、SOAPのリクエストとレスポンスを試してみた - メモ的な思考的な

実際のリクエスト・レスポンスの内容は、SOAP UI のログで確認していました。

ただ、SOAP UIが使えない環境ではどのように確認すればよいか分からなかったため、メモを残します。

 
目次

 

環境

 

ZeepのHistory Plugin

最新の送受信履歴を確認

Zeepの公式ドキュメントを読んだところ、その用途のプラグインが記載されていました。
https://python-zeep.readthedocs.io/en/master/plugins.html#historyplugin

 
試してみたところ、リクエスト・レスポンスが確認できました。

ただ、そのままではenvelopeが lxml.etree._Element オブジェクトなため、中身まで見れませんでした。

import pathlib
from zeep import Client
from zeep.plugins import HistoryPlugin

WSDL = pathlib.Path.cwd().joinpath('RequestResponse.wsdl')


history_plugin = HistoryPlugin()
client = Client(str(WSDL), plugins=[history_plugin])
response = client.service.requestMessage(userName='taro')

print(history_plugin.last_sent)
# =>
# {'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10929d988>,
#  'http_headers': {
#    'SOAPAction': '"http://example.com/HelloWorld/requestMessage"',
#    'Content-Type': 'text/xml; charset=utf-8'}}

print(type(history_plugin.last_sent['envelope']))
# => <class 'lxml.etree._Element'>

print(history_plugin.last_sent['envelope'])
# => <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x102c54a48>

 

lxml.etreeにより、Envelopeの中身を確認

データ型が history_plugin.last_sent['envelope'] であることから、 lxml.etree を使って解析してみました。

なお、 etree.tounicode()

Deprecated: use tostring(el, encoding='unicode') instead.

https://lxml.de/api/lxml.etree-module.html#tounicode

と書かれていたため、 etree.tostring() を使います。

 
SOAP UIで確認できたものと同一でした。

print(etree.tostring(history_plugin.last_sent['envelope'], pretty_print=True, encoding='unicode'))
# =>
# <soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
#   <soap-env:Body>
#     <ns0:RequestInterface xmlns:ns0="http://example.com/HelloWorld">
#       <ns0:userName>taro</ns0:userName>
#     </ns0:RequestInterface>
#   </soap-env:Body>
# </soap-env:Envelope>

 
レスポンスも同様に確認できました。

print(response)
# => Hello, taro

print(history_plugin.last_received)
# =>
# {'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x1092e1ac8>,
#  'http_headers': {
#    'Content-Type': 'text/xml; charset=utf-8',
#    'Content-Encoding': 'gzip',
#    'Content-Length': '198', 'Server': 'Jetty(6.1.26)'}}

print(type(history_plugin.last_received['envelope']))
# => <class 'lxml.etree._Element'>

print(history_plugin.last_received['envelope'])
# => <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x102c98b48>

print(etree.tostring(history_plugin.last_received['envelope'], pretty_print=True, encoding='unicode'))
# => (長いので改行あり)
# <soapenv:Envelope
#       xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
#       xmlns:hel="http://example.com/HelloWorld">
#    <soapenv:Header/>
#    <soapenv:Body>
#       <hel:ResponseInterface>
#
#          <hel:returnMessage>Hello, taro</hel:returnMessage>
#       </hel:ResponseInterface>
#    </soapenv:Body>
# </soapenv:Envelope>

 

複数件の送受信履歴を確認

複数件の送受信履歴を確認したい場合は、

By default at most one transaction (sent/received) is kept. But this can be changed when you create the plugin by passing the maxlen kwarg.

とのことです。

 
ソースコードを見たところ、collections.deque型のインスタンス変数 _buffer に結果が格納されるようでした。
https://github.com/mvantellingen/python-zeep/blob/3.1.0/src/zeep/plugins.py#L49

そのため、以下のようにして表示できました。

import pathlib

from lxml import etree
from zeep import Client
from zeep.plugins import HistoryPlugin

WSDL = pathlib.Path.cwd().joinpath('RequestResponse.wsdl')

# インスタンスを生成する時に、引数maxlenを指定
history_plugin = HistoryPlugin(maxlen=3)
client = Client(str(WSDL), plugins=[history_plugin])

# taro > jiro > saburo の順番にデータを送信
client.service.requestMessage(userName='taro')
client.service.requestMessage(userName='jiro')
client.service.requestMessage(userName='saburo')

print(history_plugin._buffer)
# =>
# deque([
#   {'received': {
#       'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10ae37a08>,
#       'http_headers': {
#           'Content-Type': 'text/xml; charset=utf-8', 'Content-Encoding': 'gzip',
#           'Content-Length': '198', 'Server': 'Jetty(6.1.26)'}},
#    'sent': {
#       'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10adf28c8>,
#       'http_headers': {
#           'SOAPAction': '"http://example.com/HelloWorld/requestMessage"',
#           'Content-Type': 'text/xml; charset=utf-8'}}},
#   {'received': {
#       'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10ae43648>,
#       'http_headers': {
#           'Content-Type': 'text/xml; charset=utf-8', 'Content-Encoding': 'gzip',
#           'Content-Length': '199', 'Server': 'Jetty(6.1.26)'}},
#    'sent': {
#       'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10ae28e88>,
#       'http_headers': {
#           'SOAPAction': '"http://example.com/HelloWorld/requestMessage"',
#           'Content-Type': 'text/xml; charset=utf-8'}}},
#   {'received': {
#       'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10ae43dc8>,
#       'http_headers': {
#           'Content-Type': 'text/xml; charset=utf-8', 'Content-Encoding': 'gzip',
#           'Content-Length': '199', 'Server': 'Jetty(6.1.26)'}},
#    'sent': {
#       'envelope': <Element {http://schemas.xmlsoap.org/soap/envelope/}Envelope at 0x10ae43088>,
#       'http_headers': {
#           'SOAPAction': '"http://example.com/HelloWorld/requestMessage"',
#           'Content-Type': 'text/xml; charset=utf-8'}}}],
#   maxlen=3)


# 3件目に saburo を送っていることを確認
buf = history_plugin._buffer[2]
print(type(buf))
# => <class 'dict'>

print(etree.tostring(buf['sent']['envelope'], pretty_print=True, encoding='unicode'))
# =>
# <soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
# <soap-env:Body>
# <ns0:RequestInterface xmlns:ns0="http://example.com/HelloWorld">
# <ns0:userName>saburo</ns0:userName>
# </ns0:RequestInterface>
# </soap-env:Body>
# </soap-env:Envelope>

 

ソースコード

GitHubに上げました。 history_plugin ディレクトリが今回のファイルです。
https://github.com/thinkAmi-sandbox/python_zeep-sample

Python + Zeep + SOAP UI + 自作WSDLで、SOAPのリクエストとレスポンスを試してみた

前回、Python + Zeepで、SOAP APIクライアントを作成しました。
Python + Zeep で SOAP API クライアントを作ってみた - メモ的な思考的な

 
そんな中、自分でもWSDLを書いてみたくなりました。

ただ、Zeepを使ってSOAP通信するには、WSDLの他にSOAPサーバが必要です。

何かいいものがないか考えたところ、同僚より SOAP UI を教わりました。SOAP UIにWSDLを食わせるとといろいろと自動生成してくれるとのことです。
The World's Most Popular Automated API Testing Tool | SoapUI

 
そのため、今回は、Python + Zeep + SOAP UI + 自作のWSDLで、SOAPのリクエストとレスポンスを試してみました。

 
目次

 

環境

 
SOAPWSDLとも他のバージョンがありましたが、Web上に資料の多い上記のバージョンとしました。

なお、Zeep・SOUP UI とも、上記のバージョンでも動作します。

 

リクエストパラメータなし・レスポンスデータありのSOAP通信

WSDLの作成

WSDLを書いたことがないため、以下の記事を参考に、ステップを追って書いてみることにしました。
WSDL:Webサービスのインターフェイス情報:Webサービスのキホン(4) - @IT

 
手始めに、まずはパラメータなしのリクエストを送ると Hello, world! が返ってくるWSDLを書くことにしました。

以降、ざっくりとしたメモ書きです。

 

wsdl:definitions要素

関係する要素のうち

  • xmlns:wsdl
  • xmlns:soap11
  • xmlns:http
  • xmlns:mime
  • xmlns:xsd

は定型のため、参考記事をそのまま書きました。

他に

  • xmlns:ns0
    • WSDL内で定義を参照するのに使う
  • targetNamespace

を追加しました。

なお、上記2つは適当な値で良さそうだったため、 http://example.com/HelloWorld をセットしました。

<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要素

基本のデータ型は xmlns:xsd に定義されています。

しかし、実際には、基本データ型を組み合わせることが多そうなので、今回は wsdl:types 要素を定義しました。

ResponseInterface型を用意し、その中にはstring型の returnMessage フィールドを用意しました。

なお、wsdl:types要素でも targetNamespace が必要になるため、wsdl:definitions要素と同じ値を設定しました。

<wsdl:types>
    <xsd:schema elementFormDefault="qualified" targetNamespace="http://example.com/HelloWorld">
        <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 要素

ここではリクエスト・レスポンスに使われる型を定義します。

今回はレスポンスデータだけがあります。

そこで、messageOutと名付けたmessageに対し、element属性にて先ほど定義したwsdl:types要素を名前空間付きで ns0:ResponseInterface と指定しました。
(以降も、同じファイル内を参照する場合は、名前空間 ns0: を先頭に付けます)

なお、nameについては、慣例的に使われていそうな parameters としました。Zeepのソースコード上には表れない名前なので、何でも良さそうです。

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

 

wsdl:portType要素

ここではリクエスト時に使われるメソッド名と、その時のリクエスト・レスポンス型を指定するようです。

今回はリクエスト時のメソッド名を requestMessage としました。

message属性については、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要素

今までの定義を取りまとめる要素のようです。今回は以下の内容で設定しました。

  • wsdl:bindingのtype属性にて、 wsdl:port 要素を参照
  • soap11:binding は定型
    • soapAction は適当で良さそう
    • styleは rpcdocument のどちらかだが、今回は wsdl:types 要素で定義した型を使ってリクエスト・レスポンスするため、 document を設定
      • 基本型だけであれば rpc なのかな...
  • wsdl:operation には、 wsdl:portTypeで定義したメソッド名 requestMessage を指定
    • 別の値にすると、リクエストがうまくいかない
  • wsdl:inputwsdl:output については、定型
<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要素

実際にアクセスする時の情報をセットします。

  • binding 属性には、 wsdl:binding のname属性をセット
  • location 属性には、実際にアクセスするURL (今回の場合は、SOAP UIのモックURL) をセット
<wsdl:service name="HelloService">
    <wsdl:port name="HelloServicePort" binding="ns0:HelloBindingSoap11">
        <soap11:address location="http://localhost:8088/mockHelloBindingSoap11"/>
    </wsdl:port>
</wsdl:service>

 

WSDL全体

以上でWSDLが書けたため、全体を載せておきます。

<?xml version="1.0" encoding="UTF-8"?>
<!--
 ns0 は、WSDL内で定義を参照するのに使われる
 targetNamespaceは、とりあえず適当に設定しておく
 -->
<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>
        <!-- ここのtargetNamespaceも適当に設定(先ほどのと同じでもOK) -->
        <xsd:schema elementFormDefault="qualified" targetNamespace="http://example.com/HelloWorld">
            <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:message>
    <wsdl:message name="messageOut">
        <!--
        独自定義の型を使用するため、element属性にて上記のelementのname属性をセット
        name属性の値は、慣例的に parameters っぽい(他の名称にしても動作する)
        -->
        <wsdl:part name="parameters" element="ns0:ResponseInterface" />
    </wsdl:message>

    <wsdl:portType name="HelloPort">
        <wsdl:operation name="requestMessage">
            <!-- リクエスト(input)とレスポンス(output)の型を特定するため、上記messageのname属性をセット -->
            <wsdl:input message="ns0:messageIn"/>
            <wsdl:output message="ns0:messageOut"/>
        </wsdl:operation>
    </wsdl:portType>

    <!-- 上記のportTypeを使うため、type属性にはportTypeのname属性をセット -->
    <wsdl:binding name="HelloBindingSoap11" type="ns0:HelloPort">
        <!-- 独自の型定義を使っているため、styleには document をセット -->
        <soap11:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <!-- portTypeの中にあるoperationのnameと同じ値をセット(今回の場合、requestMessage) -->
        <wsdl:operation name="requestMessage">
            <!-- soapAction は適当な値で良さそう -->
            <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">
        <!-- binding属性には、上記bindingのname属性をセット -->
        <wsdl:port name="HelloServicePort" binding="ns0:HelloBindingSoap11">
            <!-- 実際にアクセスするURL(今回はSOAP UI のモックURL)をセット -->
            <soap11:address location="http://localhost:8088/mockHelloBindingSoap11"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

 

SOAP UI によるモックの作成

モックの作り方については以下が参考になりました。ありがとうございます。
SOAP UIのMock Serviceを使った効率的なWebサービスのテスト - そごうソフトウェア研究所

 
現在の SOAP UIでは、プロジェクト作成時にはモックを作れないようですが、後でコンテキストメニューから追加しました。

 
モックとして返す内容は、以下の通り Hello, world! としました。

<soapenv:Envelope
   xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:hel="http://example.com/HelloWorld">
   <soapenv:Header/>
   <soapenv:Body>
      <hel:ResponseInterface>
         <!--Optional:-->
         <hel:returnMessage>Hello, world!</hel:returnMessage>
      </hel:ResponseInterface>
   </soapenv:Body>
</soapenv:Envelope>

 

Zeepによる実装

前回試した通り、Zeepで実装します。なお、 get_type() は使わない方の実装としました。

import pathlib

from zeep import Client

WSDL = pathlib.Path.cwd().joinpath('Hello.wsdl')


client = Client(str(WSDL))
response = client.service.requestMessage()

print(type(response))
print(response)

 

動作確認

Pythonスクリプトを実行すると、レスポンスがありました。

$ python run_hello.py
<class 'str'>
Hello, world!

 
SOAP UIの方にも、左側にリクエストデータが記録されていました。

f:id:thinkAmi:20181104174053p:plain:w350

全体が見えないため、内容を貼っておきます。

<?xml version='1.0' encoding='utf-8'?>
<soap-env:Envelope 
    xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body/>
</soap-env:Envelope>

 

リクエストパラメータあり・レスポンスデータありのSOAP通信

続いて、リクエストパラメータがあるバージョンも作ってみます。

ユーザ名を送信すると Hey, <ユーザ名> と返すものを作ってみます。

 

WSDLの作成

変更があるのは、 wsdl:typeswsdl:message です。

<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">
        ...
</wsdl:types>

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

あとは同様です。

 

SOAP UIのモックを修正

修正したWSDLを元にモックを作成します。

今回はリクエストパラメータを取得して返すので、

を行います。

方法については以下が参考になりました。

 
まずは、レスポンスのXMLHello, ${userName} を追加します。

<soapenv:Envelope
   xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:hel="http://example.com/HelloWorld">
   <soapenv:Header/>
   <soapenv:Body>
      <hel:ResponseInterface>
         <!--Optional:-->
         <hel:returnMessage>Hello, ${userName}</hel:returnMessage>
      </hel:ResponseInterface>
   </soapenv:Body>
</soapenv:Envelope>

 
また、Scriptにも以下のGroovyコードを追加します。

def holder = new com.eviware.soapui.support.XmlHolder( mockRequest.requestContent )
def userName = holder.getNodeValue("//*:userName")
requestContext.userName = userName

 

Zeepの実装

引数に userName を追加するだけです。今回は taro をリクエスト値として追加します。

import pathlib

from zeep import Client

WSDL = pathlib.Path.cwd().joinpath('RequestResponse.wsdl')


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

print(type(response))
print(response)

 

実行結果

リクエストとレスポンスが成功しました。

$ python run_request_response.py
<class 'str'>
Hello, taro

 
SOAP UI にもログが記録されていました。

<?xml version='1.0' encoding='utf-8'?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
  <soap-env:Body>
    <ns0:RequestInterface xmlns:ns0="http://example.com/HelloWorld">
      <ns0:userName>taro</ns0:userName>
    </ns0:RequestInterface>
  </soap-env:Body>
</soap-env:Envelope>

 

参考資料

 

ソースコード

GitHubに上げました。 wsdls_and_client ディレクトリ以下が今回のものです。
https://github.com/thinkAmi-sandbox/python_zeep-sample

Python + Zeep で SOAP API クライアントを作ってみた

Python + Zeep で SOAP API クライアントを作ってみたため、その時のメモを残します。

 

目次

 

環境

 
なお、WSDLがあるSOAP APIを使うことを想定しています。

WSDLのバージョンなどはWikipediaが参考になりました。
Web Services Description Language - Wikipedia

 

Zeepとは

公式ドキュメントによると

A fast and modern Python SOAP client Highlights:

  • Compatible with Python 2.7, 3.3, 3.4, 3.5, 3.6 and PyPy
  • Build on top of lxml and requests
  • Support for Soap 1.1, Soap 1.2 and HTTP bindings
  • Support for WS-Addressing headers
  • Support for WSSE (UserNameToken / x.509 signing)
  • Support for tornado async transport via gen.coroutine (Python 2.7+)
  • Support for asyncio via aiohttp (Python 3.5+)
  • Experimental support for XOP messages

https://python-zeep.readthedocs.io/en/master/

とのことで、いい感じのSOAP API クライアントが作れそうでした。

 

使えそうな SOAP APIを探す

SOAP API サーバを自作することも考えたのですが、ゼロから作るには先が長くなりそうでした。

 
そのため、使えそうな SOAP Web API を探してみました。

有名なところではFlickrAPIがありました。
https://www.flickr.com/services/api/

ただ、WSDLがなかったため、今回は見送りました。

 
他のAPIを探したところ、以下がありました。

 
今回はWSDLを使ってリクエスト/レスポンスができればよいため、デ辞蔵Webサービス - SOAPAPI を使います。

 

mzeepオプションにて、WSDLを解析する

WSDLXMLで書かれていますが、開発するために読み解くのは手間がかかります。

Zeepでは、 python -mzeep <WSDLのパス> で、WSDLファイルを解析できます。

今回のWSDLファイルは上記サイトに記載されていたため、ダウンロードして実行します。

$ python -mzeep SoapServiceV11.xml > wsdl.txt

 
wsdl.txtを開くと、

Prefixes:
     xsd: http://www.w3.org/2001/XMLSchema
...

Global elements:
     ns0:ArrayOfDicInfo(ns0:ArrayOfDicInfo)
...

Global types:
     xsd:anyType
     ns0:ArrayOfDicInfo(DicInfo: ns0:DicInfo[])
...
Bindings:
     Soap11Binding: {http://MyDictionary.jp/SOAPServiceV11}SOAPServiceV11Soap
...

Service: SOAPServiceV11
     Port: SOAPServiceV11Soap (Soap11Binding: {http://MyDictionary.jp/SOAPServiceV11}SOAPServiceV11Soap)
         Operations:
            GetDicItem(AuthTicket: xsd:string, DicID: ns1:guid, ItemID: xsd:string, LocID: xsd:string, ContentProfile: ns0:ContentProfile, QueryListForHighLight: ns0:ArrayOfQuery) -> GetDicItemResult: ns0:DicItem
...

と、各種情報が分かりやすくなりました。

 

型情報を見やすくする

mzeepオプションでだいぶ分かりやすくなりました。

ただ、型情報が1行で表示されるため、複数の項目を持つ型の場合、見づらいことに気づきました。

そのため、

import pathlib


read_file = pathlib.Path('./wsdl.txt')
with read_file.open(mode='r') as r:
    f = r.read()

formatted = f.split(',')

write_file = pathlib.Path('./formatted.txt')
with write_file.open(mode='w') as w:
    for f in formatted:
        w.write(f'{f.strip()}\n')

のようなスクリプトを作成し、フォーマットしてみました。

実行前

ns0:DicInfo(DicID: ns1:guid, FullName: xsd:string, ShortName: xsd:string, Publisher: xsd:string, Abbrev: xsd:string, StartItemID: xsd:string, ScopeList: ns0:ArrayOfScope, SearchOptionList: ns0:ArrayOfSearchOption, DefSearchOptionIndex: xsd:int, ItemMapList: ns0:ArrayOfString)

実行後

ns0:DicInfo(DicID: ns1:guid
FullName: xsd:string
ShortName: xsd:string
Publisher: xsd:string
Abbrev: xsd:string
StartItemID: xsd:string
ScopeList: ns0:ArrayOfScope
SearchOptionList: ns0:ArrayOfSearchOption
DefSearchOptionIndex: xsd:int
ItemMapList: ns0:ArrayOfString)

 
いろいろと手を加えたいところですが、今はこのくらいで十分なので、良しとします。

 

ZeepでAPIクライアントを作ってみる

辞書検索サービスの使い方にあるように、まずは

GetDicListで呼び出し可能な辞書の一覧を取得

をZeepを使って作ってみます。

 

clientの生成

公式ドキュメントの「A simple use-case」に従い、今回のSOAPクライアントを生成してみます。
https://python-zeep.readthedocs.io/en/master/#a-simple-use-case

import pathlib
from zeep import Client

WSDL = pathlib.Path.cwd().joinpath('SoapServiceV11.xml')

client = Client(str(WSDL))

 

SOAP APIのメソッドを呼び出す (client.get_type()を使う)

クライアントができたため、次は SOAP API のメソッドを呼び出してみます。

今回使う GetDicList メソッドでは、引数 AuthTicket を持つことが分かっています。

そのため、引数を渡すような実装方法を見たところ、公式ドキュメントの「Creating objects - Datastructures」に記載がありました。
https://python-zeep.readthedocs.io/en/master/datastructures.html#creating-objects

 
引数 AuthTicket の型は xsd:string ですので、 client.get_type('xsd:string') で型用のオブジェクトを生成しておきます。

xsd_string = client.get_type('xsd:string')

 
あとは、Zeepが client.serviceSOAP APIのメソッドをいい感じに生成・実行してくれます。

response = client.service.GetDicList(AuthTicket=xsd_string(''))
print(response)

 
ここまでのコード全体は以下です。

import pathlib
from zeep import Client

WSDL = pathlib.Path.cwd().joinpath('SoapServiceV11.xml')

client = Client(str(WSDL))
xsd_string = client.get_type('xsd:string')

response = client.service.GetDicList(AuthTicket=xsd_string(''))
print(response)

 
実行結果です。いい感じの結果が返ってきました。

$ python run_GetDicList.py 
[{
    'DicID': 'xxxx',
    'FullName': 'Edict和英辞典',
    'ShortName': 'Edict和英辞典',
...

 

SOAP APIのメソッドを呼び出す (client.get_type()を使わない)

上記で実装ができていましたが、型オブジェクトを生成するために

xsd_string = client.get_type('xsd:string')

としているのが手間でした。

 
よく見ると、公式ドキュメントの「Creating objects - Datastructures」には続きがあり、

However instead of creating an object from a type defined in the XSD you can also pass in a dictionary. Zeep will automatically convert this dict to the required object (and nested child objects) during the call.

とのことでした。

 
そのため、先ほどのコードは

client = Client(str(WSDL))

response = client.service.GetDicList(AuthTicket='')
print(response)

でもOKです。

 
実行結果も同じでした。

$ python run_GetDicList.py 
[{
    'DicID': 'xxxx',
    'FullName': 'Edict和英辞典',
    'ShortName': 'Edict和英辞典',
...

 

引数の型が複雑な SOAP API を呼び出す (get_type()を使う)

先ほどの GetDicList APIは、型が xsd:string と単純なものでした。

引数の型が複雑なAPIを探したところ、 SearchDicItem がありました。

 
そこで、まずは get_type() を使う書き方で実装してみます。

import pathlib
from zeep import Client

WSDL = pathlib.Path.cwd().joinpath('SoapServiceV11.xml')


def get_guid_list_from_api():
    client = Client(str(WSDL))

    response = client.service.GetDicList(AuthTicket='')
    return [response[0]['DicID'], response[1]['DicID']]


def call_api_with_get_type():
    def create_query(word):
        ns0_merge_option = client.get_type('ns0:MergeOption')
        ns0_match_option = client.get_type('ns0:MatchOption')

        query = client.get_type('ns0:Query')(
            Words=xsd_string(word),
            ScopeID=xsd_string('HEADWORD'),
            MatchOption=ns0_match_option('EXACT'),
            MergeOption=ns0_merge_option('OR')
        )
        return query

    client = Client(str(WSDL))
    xsd_string = client.get_type('xsd:string')
    xsd_unsigned_int = client.get_type('xsd:unsignedInt')
    ns1_guid = client.get_type('ns1:guid')

    guid_list = get_guid_list_from_api()
    guids = client.get_type('ns0:ArrayOfGuid')([
        ns1_guid(guid_list[0]),
        ns1_guid(guid_list[1]),
    ])
    queries = client.get_type('ns0:ArrayOfQuery')([
        create_query('apple'),
        create_query('america'),
    ])

    response = client.service.SearchDicItem(
        AuthTicket=xsd_string(''),
        DicIDList=guids,
        QueryList=queries,
        SortOrderID=xsd_string(''),
        ItemStartIndex=xsd_unsigned_int('0'),
        ItemCount=xsd_unsigned_int('2'),
        CompleteItemCount=xsd_unsigned_int('2'),
    )

    for r in response['ItemList']['DicItem']:
        print(r['Title']['_value_1'].text)
        print(dir(r['Title']['_value_1']))
        print('=' * 5)

 
ここで、ネスト & 配列を持つ引数 QueryList について取り上げてみます。

まず、配列の要素である ns0:Query 型のオブジェクトを作ります。

def create_query(word):
    ns0_merge_option = client.get_type('ns0:MergeOption')
    ns0_match_option = client.get_type('ns0:MatchOption')

    query = client.get_type('ns0:Query')(
        Words=xsd_string(word),
        ScopeID=xsd_string('HEADWORD'),
        MatchOption=ns0_match_option('EXACT'),
        MergeOption=ns0_merge_option('OR')
    )
    return query

 
次に、配列となる ns0:ArrayOfQuery を生成します。

queries = client.get_type('ns0:ArrayOfQuery')([
    create_query('apple'),
    create_query('america'),
])

 
最後に、SOAP APIの引数として渡します。

response = client.service.SearchDicItem(
    QueryList=queries,
    ...
)

 
実行結果は以下の通りです。

レスポンスが返ってきているので、WSDLでのやり取りは成功しているんだろうな程度にしておき、深入りはしないようにします。

$ python run_SearchDicItem.py 
apple
['__bool__', '__class__', '__contains__', ...]
=====
America
...

 

引数の型が複雑な SOAP API を呼び出す (get_type()を使わない)

上記の通り、 client.get_type() を使って実現できましたが、それぞれの型を get_type() で生成しておかなければならないため、いろいろと手間に感じました。

そこで、先ほどと同じように client.get_type() を使わないやり方で実装してみます。

# importや get_guid_list_from_api() は省略

def call_api_without_get_type():
    def create_query(word):
        return {
            'Words': word,
            'ScopeID': 'HEADWORD',
            'MatchOption': 'EXACT',
            'MergeOption': 'OR',
        }

    client = Client(str(WSDL))
    guids = {'guid': get_guid_list_from_api()}
    queries = {
        'Query': [
            create_query('apple'),
            create_query('america'),
        ]
    }

    response = client.service.SearchDicItem(
        AuthTicket='',
        DicIDList=guids,
        QueryList=queries,
        SortOrderID='',
        ItemStartIndex=0,
        ItemCount=2,
        CompleteItemCount=2,
    )

    for r in response['ItemList']['DicItem']:
        print(r['Title']['_value_1'].text)
        print(dir(r['Title']['_value_1']))
        print('=' * 5)

 
では、複雑な型を持つ引数 QueryList について、 get_type() を使う場合との差分を見ていきます。

def create_query(word):
    ns0_merge_option = client.get_type('ns0:MergeOption')
    ns0_match_option = client.get_type('ns0:MatchOption')

    query = client.get_type('ns0:Query')(
        Words=xsd_string(word),
        ScopeID=xsd_string('HEADWORD'),
        MatchOption=ns0_match_option('EXACT'),
        MergeOption=ns0_merge_option('OR')
    )
    return query

queries = client.get_type('ns0:ArrayOfQuery')([
    create_query('apple'),
    create_query('america'),
])

def create_query(word):
    return {
        'Words': word,
        'ScopeID': 'HEADWORD',
        'MatchOption': 'EXACT',
        'MergeOption': 'OR',
    }

queries = {
    'Query': [
        create_query('apple'),
        create_query('america'),
    ]
}

となりました。

ポイントは

  1. create_query() のような dict を用意 (これがArrayOfQueryの要素)
  2. keyに Query を、valueに 1. で作成した要素を持つdictを用意し、それを配列の要素とすることで、ArrayOfQuery になる

です。

get_type() を使うよりも、かなり簡潔になり、見やすくなりました。

 
以上より、Zeepによる SOAP APIのリクエスト/レスポンスができました。

 

ソースコード

GitHubに上げました。 dejizo_client ディレクトリ以下が今回のファイルです。
https://github.com/thinkAmi-sandbox/python_zeep-sample

なお、WSDLファイルをリポジトリに含めていません。含めてよいか分からなかったためです。