Edited at
NSSOLDay 25

Swagger CodegenでPythonのクライアントコードを生成する

Swagger Codegenを使って、あるシステム用のPythonのクライアントコードを生成した。そこで得られたノウハウを紹介したい。


Swagger Codegenのバージョン

現時点で、Swagger Codegenの主なバージョンは次のとおりである。


  • 3.0.4-SNAPSHOT

  • 3.0.3 (current stable)

  • 2.4.1-SNAPSHOT

  • 2.4.0 (current stable)

  • 2.3.1

  • (それ以前)

3.0.4と3.0.3はpython用のクライアントコード生成をサポートしていない。

2.4.0は長らくSNAPSHOTだったが、2018-11-30にstableになった。今python用のクライアントを生成するなら、2.4.0か2.4.1だ。


Python 3.7のサポート

Pythonは3.7でasyncを予約語とした。これはもう識別子としては使えない。しかし、2.3.1の生成するコードはasyncを識別子に使っているため、Python 3.7では実行できない。SNAPSHOT時代の2.4.0もずっと同じ状況だったが、ある時点で識別子名が変更され、Python 3.7でも実行できるようになった。

クライアント環境でPython 3.7を対象とするなら、今となっては2.4.0 stableを使えばよい。それ以前のバージョンを使う場合は、生成されたコードを自分で書き換えてしまうのが手軽である。次のようにする。

find "${OUTPUT_DIR}/client/" -type f -name \*.py -exec sed -i 's/async=/async_req=/g' {} +

find "${OUTPUT_DIR}/client/" -type f -name \*.py -exec sed -i 's/async bool/async_req bool/g' {} +
find "${OUTPUT_DIR}/client/" -type f -name \*.py -exec sed -i "s/'async'/'async_req'/g" {} +
sed -i "s/if not async/if not async_req/g" "${OUTPUT_DIR}/client/api_client.py"

これはKubernetesのPythonクライアントに出されたissueであるProblem with python 3.7 and "async" as parameter nameで知った。


デフォルトのタイムアウト

クライアントからapiを呼び出す際、キーワード引数_request_timeoutを指定すると、その呼び出しのタイムアウトを設定できる。

しかし、それは個別の呼び出しのタイムアウトであり、デフォルトのタイムアウト、というものは設定できない。そこで、生成されたコードを自分で書き換えて実現した。

timeout = None

if _request_timeout:
if isinstance(_request_timeout, (int, ) if six.PY3 else (int, long)): # noqa: E501,F821
timeout = urllib3.Timeout(total=_request_timeout)
elif (isinstance(_request_timeout, tuple) and
len(_request_timeout) == 2):
timeout = urllib3.Timeout(
connect=_request_timeout[0], read=_request_timeout[1])

これはSwagger Codegenが生成したrest.pyの一部だ。上記コードの1行目を

timeout = self.pool_manager.connection_pool_kw.get('timeout')

と変更する。そして、ユーザーコードの適当な箇所で

api_client = ApiClient()

api_client.rest_client.pool_manager.connection_pool_kw['timeout'] = 30

とすれば、このApiClientを使い回す限り、このタイムアウト指定がデフォルトとして適用される。


デフォルトのリトライ

タイムアウトとは異なり、デフォルトのリトライ指定は生成コードを書き換えなくても可能だ。ユーザーコードの適当な箇所で

api_client = ApiClient()

api_client.rest_client.pool_manager.connection_pool_kw['retries'] = False

としておけば、このApiClientを使い回す限り、このリトライ指定がデフォルトとして適用される。


モデルのdict変換

Swagger Codegenが生成するコードでは、apiの引数と返値はモデルで表現されている。モデルはsetterとgetterを持った、Pythonのクラスである。

しかし引数と返値は元々はJSON形式であったりするので、モデルをJSONあるいはそれと同等なdictに変換して扱いたいこともある。

そのために、各モデルには自分をdictに変換するためのto_dict()というメソッドが生成されている。だが、Swagger Codegenのどのバージョンでも正しく動作しない時がある。具体的には、モデルのプロパティが辞書になっていて、その値が別のモデルのリストであるような場合だ。

そこで、次のようなコードを書いた。モジュールレベルの関数である。

def to_dict_value(value):

if hasattr(value, 'to_dict'):
return to_dict(value)
if isinstance(value, list):
return [to_dict_value(x) for x in value]
if isinstance(value, dict):
return {k: to_dict_value(v) for k, v in value.items()}
return value

def to_dict(model):
result = {}
for attr, _ in six.iteritems(model.swagger_types):
value = getattr(model, attr)
result[attr] = to_dict_value(value)
return result

このto_dict()にモデルを渡せば、そのような場合でも正常にdictに変換される。


生成コードのパッケージ

Swagger Codegenが生成するコードのパッケージは、ジェネレータ実行時に-DpackageNameオプションで指定する。

パッケージが1階層ならば問題ないが、サブパッケージを含めて指定すると、出力が妙なことになる。例えば、-DpackageName=pkg.subpkgと指定すると、pkg/subpkgというディレクトリとpkg.subpkgというディレクトリが作成され、その2つに分かれる形でコードが生成される。

けれども、これは出力先が変なだけで、ファイルは正しくpkg.subpkgというサブパッケージ下で動作するものが生成されている。なので、生成後に2つのディレクトリを統合してしまえばよい。