Edited at

connexionを使ってPython APIサーバのAPI定義と実装を関連付ける


connexion

connexionはOASを利用してAPIエンドポイントとpython関数をmapするためのpythonパッケージです。

Flaskベースで作成されており、通常flaskのdecorationで @app.route("/") などとエンドポイントを指定してHTTPリクエストと関数の関係を記述していたものを、OAS側に記載することができます。

何が嬉しいかというと、個人的にはAPI定義を元にルーティングが決定されるためAPI定義と実装の乖離が生じない点だと思います。あとはAPI定義に書いてあればValidationとかもしてくれるみたいです。

connexionは基本的にはFlaskで動作しますが他のpython webサーバでも動作します。

2019年4月現在、次のサーバが利用できるようです。


  • flask

  • tornado

  • aiohttp

  • gevent

https://github.com/zalando/connexion


使ってみる


環境

$ sw_vers

ProductName: Mac OS X
ProductVersion: 10.14.3
BuildVersion: 18D109
$ python --version
Python 3.7.2


setup

repositoryに入っているexampleを動かしてみます。

今回はOpenAPI Specification v3.0で記述されたhelloworldを動かします。

$ git clone https://github.com/zalando/connexion.git

$ cd connexion/examples/openapi3/helloworld/
$ pip install connexion
$ pip show connexion | grep '^Version'
Version: 2.2.0

OASの中身は次のようになっています。

APIは1つだけエンドポイント/greeting/{name}を持ち、HTTP POSTリクエストを投げると"hello {name}"の文字列が返ってくることが記述されています。

特徴的なのはoperationIdで、この情報をもとにconnexionが呼び出す関数を決定します。


openapi/helloworld-api.yaml

openapi: "3.0.0"

info:
title: Hello World
version: "1.0"
servers:
- url: http://localhost:9090/v1.0

paths:
/greeting/{name}:
post:
summary: Generate greeting
description: Generates a greeting message.
operationId: hello.post_greeting
responses:
200:
description: greeting response
content:
text/plain:
schema:
type: string
example: "hello dave!"
parameters:
- name: name
in: path
description: Name of the person to greet.
required: true
schema:
type: string
example: "dave"



起動

起動してみます。

$ python hello.py

The swagger_ui directory could not be found.
Please install connexion with extra install: pip install connexion[swagger-ui]
or provide the path to your local installation by passing swagger_path=<your path>

The swagger_ui directory could not be found.
Please install connexion with extra install: pip install connexion[swagger-ui]
or provide the path to your local installation by passing swagger_path=<your path>

* Serving Flask app "hello" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:9090/ (Press CTRL+C to quit)

この状態で次のようにHTTPリクエストを投げてやると、connexion(Flask)サーバがlocalhost:9090で待ち受けていて期待した通りのResponseが得られます。

$ curl -X POST http://localhost:9090/v1.0/greeting/hoge

Hello hoge

Warningに書いてあるように、商用環境で利用する際はWSGIサーバを利用してください。

http://flask.pocoo.org/docs/1.0/deploying/


中身の確認

hello.pyの中身は次のようになっています。やっていることは、


  1. connexionのFlaskAppインスタンスを作成

  2. openapi/helloworld-api.yamlからOASを読み込み

  3. サーバ起動

  4. operationId: hello.post_greetingのリクエストを捌く関数を定義


hello.py

#!/usr/bin/env python3


import connexion

def post_greeting(name: str) -> str:
return 'Hello {name}'.format(name=name)

if __name__ == '__main__':
app = connexion.FlaskApp(__name__, port=9090, specification_dir='openapi/')
app.add_api('helloworld-api.yaml', arguments={'title': 'Hello World Example'})
app.run()


flaskで書くとこんな感じでしょうか。

APPLICATION_ROOTが違いますが、開発用サーバがROOTを変えられないようなので大目に見てください(WSGI使いましょう)


hello.py

#!/usr/bin/env python3

import flask
app = flask.Flask(__name__)

@app.route('/greeting/<name>', methods=['POST'])
def post_greeting(name: str) -> str:
return 'Hello {name}'.format(name=name)

if __name__ == '__main__':
app.run(port=9090)


flaskではOASを読みに行かないためAPI定義とソースコード間に関連がないので、API定義を見ながら実装は独立してすることになります。

独立というと耳障りは良いですが実装はAPI仕様を反映しているべきで、そこを自動的に関連付けてくれることがconnexionのメリットなのかなと思います。


Tips


デフォルトのhostが違う

FlaskのApp.run()はhostパラメータを指定しなかった場合 127.0.0.1 でLISTENしますが、connexionは 0.0.0.0 でLISTENします。

大きな違いはないのですが、ちょっと不自然です。issueで質問しているのでわかったら書きます。

Flaskはここでhostパラメータ未指定の際のデフォルト値を指定しています

https://github.com/pallets/flask/blob/master/flask/app.py#L924

一方connexionも独自にデフォルト値を定義しているのでFlaskと違う挙動になっています。

https://github.com/zalando/connexion/blob/master/connexion/apps/flask_app.py#L83


FlaskAppに他サーバの実装が記述されている

この実装はどうなんだろう🤔


FlaskApp

        if self.server == 'flask':

(snip)
elif self.server == 'tornado':
(snip)
elif self.server == 'gevent':

https://github.com/zalando/connexion/blob/master/connexion/apps/flask_app.py#L94


まとめ

connexionを動かしてAPIと実装を分離してOASからその関連付けができることを見ました。

まだconnexion自身に怪しい部分がありますが、OASを書くなら使ってみてもいいかもしれません。