1. はじめに

ここでは、openapi-generator-cli を利用して、Ruby以外のプログラミング言語、Python用のテンプレート(雛形)コードを生成する方法を説明します。

2. 利用するOpenAPI定義ファイル

チュートリアル「OpenAPI+Sinatraによる検索ページの作成 」で利用した、openapi.yaml ファイルを利用します。

この openapi.yaml ファイルを元に、openapi-generator-cli から、Python用のコードを出力します。

3. 作業の概要

  1. ゼミ室10のThinkPadのいずれかにログインします。

  2. Ruby用と同様にGitHubのプロジェクトをコピー(clone)します。

  3. openapi-generatorコマンドを操作し、希望するプログラミング言語用のテンプレートコードを出力します。

  4. 動かし方を確認し、必要な改造を加えていきます。

この作業を始める前に、チュートリアルOpenAPI+Sinatraによる検索ページの作成 の内容は一通り実施しておいてください。

3.1. プロジェクトのcloneと、テンプレートコードの生成

どのようなプログラミング言語(とフレームワーク)が選択できるかは、github.com/OpenAPITools#Overviewの"API stubs"に掲載されています。

まずThinkpad上の適当なディレクトリで、docker-sccp-sinatra-sampleをコピー(clone)します。

$ git clone https://github.com/YasuhiroABE/docker-sccp-sinatra-sample.git docker-sccp-python-sample
$ cd docker-sccp-python-sample

このプロジェクト自体は、Ruby言語(+Sinatra)を想定しているため、ここからは手作業で進めていきます。

3.2. Python用のテンプレートコードの出力

先ほど説明した"API stubs"の一覧には、"Python (Flask)"の記述がありますが、openapi-generator上では次のように確認できます。

$ openapi-generator-cli list | egrep 'python|^[A-Z]'

次のような出力が得られるはずです。

The following generators are available:
CLIENT generators:
    - python
    - python-experimental (experimental)
    - python-legacy
SERVER generators:
    - python-aiohttp
    - python-blueplanet
    - python-fastapi (beta)
    - python-flask
DOCUMENTATION generators:
SCHEMA generators:
CONFIG generators:

ここで利用するのは、SERVER generators にリストされている機能です。

3.3. python-flaskを試す

ドキュメントが多いのは、flaskを利用したものだと思います。 ここではPython言語とFlaskフレームワークを利用することにします。

Makefileをcatやlessコマンドなどで確認すると、gen-code:タスクの中に、ruby-sinatra用のコマンドが書かれているので、真似をしてpython-flask用のコードを生成していきます。

なお、flaskの概要については、Flaskへ ようこそ が参考になると思います。

flaskの内部で、openapi.yamlを処理する機能は、connexionライブラリ を利用しています。

$ openapi-generator-cli generate -g python-flask -o code -i openapi.yaml

次のようなメッセージが表示されます。

[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] WARN  o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
...
[main] INFO  o.o.codegen.AbstractGenerator - writing file /nfs/home/professor/yasu-abe/git/docker-sccp-python-sample/code/.openapi-generator/VERSION

最終的に code/ ディレクトリが生成されていれば成功です。

次にこの生成されたディレクトリに移動し、動かしてみます。

$ cd code/
Dockerfile   openapi_server  requirements.txt  test-requirements.txt
git_push.sh  README.md       setup.py          tox.ini

このプロジェクトでは、既にDockerfileなど、ruby-sinatraでは別途用意したファイルがあらかじめ準備されています。 続いて、README.mdを開いて、どのように利用すれば良いのか、ヒントがないか探します。

$ less README.md

すると、すぐに次のような説明が見つかると思います。

## Usage

To run the server, please execute the following from the root directory:

pip3 install -r requirements.txt
python3 -m openapi_server

and open your browser to here:

http://localhost:8080/ui/

Your OpenAPI definition lives here:

http://localhost:8080/openapi.json

この手順に従って、まず動かすことができるか確認します。

$ pip3 install -r requirements.txt
$ python3 -m openapi_server

何らかのエラーが表示されるので、それぞれ対応します。

3.3.1. 【エラー1】_spec_get がない場合

python3 -m openapi_server を実行した後に、次のようなメッセージが表示されていることを確認します。

Failed to add operation for GET /.spec
Traceback (most recent call last):
...
  File "/home/professor/yasu-abe/.local/lib/python3.6/site-packages/connexion/utils.py", line 68, in deep_getattr
    return functools.reduce(getattr, attrs, obj)
AttributeError: module 'openapi_server.controllers.default_controller' has no attribute '_spec_get'

has no attribute '_spec_get' と表示されている場合には、次のように対応します。

最後にあったメッセージのとおり、"_spec_get"という属性がないといっているので、openapi_server/controllers/default_controller.pyを確認します。

$ less openapi_server/controllers/default_controller.py

この中に、次のようなコードが含まれています。

...
def spec_get():  # noqa: E501
    """spec_get

    providing the openapi schema YAML file. # noqa: E501


    :rtype: None
    """
    return 'do some magic!'

def spec_get():は見えますが、先頭にアンダースコアがついた、_spec_get()はないようです。 おそらく、これは、openapi.yamlにURLのpathを/.specとしたために、先頭のピリオド('.')が、アンダースコアに変換されて、その名前で対応する関数を探していたためだと思われます。

このファイルの最後に、次のような2行を追加しておきます。

def _spec_get():
  return spec_get()

あるいは、../openapi.yaml ファイルを編集して、path: /spec と定義を変更して、再度 openapi-generator-cli を実行します。

3.3.2. 【エラー2】itsdangerousモジュールのimport jsonがエラーになる

python3 -m openapi_server を実行して、次のようなメッセージが表示された場合の対応を説明します。

  File "/home/yasu/.local/lib/python3.8/site-packages/flask/json/__init__.py", line 15, in <module>
    from itsdangerous import json as _json
ImportError: cannot import name 'json' from 'itsdangerous' (/home/yasu/.local/lib/python3.8/site-packages/itsdangerou
s/__init__.py)

この場合には、flaskと関連するモジュールのバージョンを以下のように変更します。

requirements.txt の最後に以下のようなコードを*追加* します。 この時に重複する Flask == 1.1.2 の行は削除してください。

Flask == 1.1.4
itsdangerous == 1.1.0
markupsafe == 1.1.1
werkzeug == 2.0.3

この状態で再度モジュールを導入すると起動するはずです。

$ pip3 install -r requirements.txt
$ python3 -m openapi_server

3.4. サーバーの起動

ここまでで、修正がうまくいけば、$ python3 -m openapi_server を実行することでサーバーが起動し、次のようなメッセージを表示するはずです。

 * Serving Flask app "__main__" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)

次のようなURLをブラウザに入力するか、クリックし、

リモートで作業している場合には、ThinkPadのIPアドレス(192.168.100.xx)を指定します。 次のようなコマンドで、自分のIPアドレスを表示して確認してください。

$ ip addr | grep 192.168
    inet 192.168.100.26/24 brd 192.168.100.255 scope global dynamic noprefixroute enp0s25

IPアドレスが 192.168.100.29 の場合には、URL は http://192.168.100.29:8080/search?q= になります。

いずれにしても、正しいURLを入力するとWebブラウザに "do some magic!" と表示されます。

3.5. テンプレートコードの編集

自動的に生成されたプログラムでは画面に文字を表示するところまで出来またい。 ここから実用的なアプリケーションにするために変更を加えていきます。

先ほど _spec_getの問題を修正するために編集した、openapi_server/controllers/default_controller.py ファイルを編集します。 Ruby+Sinatraで行なったのと同じような機能を持たせるためには、概ね、次のような機能をPythonで実装する必要があります。

  1. Search Engine本体のURLへアクセスし、結果を取得する機能

  2. 出力するHTMLについては、RubyやPythonといった言語に依存しないので、そのまま流用可能

これらの機能を順番に実装していきます。

3.5.1. 検索エンジンにアクセスし、結果を表示する

Ruby+Sinatraで検索結果を取得するために利用したコードは次のようなものでした。

## Ruby言語での処理内容
  require 'uri'
  require 'httpclient'
  client = HTTPClient.new
  url = URI::HTTPS.build({:host => "opm00h.u-aizu.ac.jp", :path => '/solr/api/v1/search', :query => "q=sccp&wt=json"})
  ret = client.get(url)
  result = JSON.parse(ret.body)
  result.to_json

これをPythonでも実装すれば、同様の機能が手に入ります。

## Python言語での同等のコード
    import urllib.request
    import urllib.parse
    url = 'https://opm00h.u-aizu.ac.jp/solr/api/v1/search?%s&wt=json' % (urllib.parse.urlencode(dict(q=q)))
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as res:
      body = res.read()
      return body
    return "{}"

このコードを、return "do some magic" の行を削除してから、その位置に上記のPython用のコードを挿入します。

pythonで外部のHTTPサーバーにアクセスするためのライブラリを検索すると、urllibと、http.clientの2つのライブラリが見つかりましたが、今回はurllibのみ利用しました。

ただ、このままでは、不十分で、出力形式が、"Content-Type: application/json" となっていて、本来は、"Content-Type: text/html"を期待しています。 そのため、openapi_server/openapi/openapi.yaml ファイルを編集し、/searchに対応するresponsesの項目を、次のように変更します。

      responses:
        "200":
          description: 200 response
          content:
            text/html:
              schema:
                type: string
      x-openapi-router-controller: openapi_server.controllers.default_controller

この状態で、$ python3 -m openapi_serverを再度実行すると、期待するよう結果が表示されます。

q=パラメータに検索対象の文字列を付与すると、変更が加わって問題なく動作することが分かります。

$ curl http://localhost:8080/search?q=sccp

3.6. さらなる改良に向けて

ここから、Webページとして、ちゃんと出力結果がみえるよう、header.html, footer.htmlなどを出力し、体裁を整えると、Ruby+Sinatraのチュートリアルと同様のアプリケーションが作成できます。

例えば、先ほどのコードでは、body = res.read()の次に、return bodyreturn "{}"がありましたが、これを削除して、次のコードに置き換えると、検索結果を箇条書きで表示してくれます。

    import json
    body_json = json.loads(body)
    output = "<ul>"
    for item in body_json["response"]["docs"]:
      output += "<li><a href=\"%s\">%s</a>%s</li>" % (item["id"], item["id"], item["title"][0])
    output += "<ul>"
    return output

4. 新しい言語を利用する際の留意点

プログラミング言語は、似ているものもから、まったく異なるパラダイム(思想)によって作成されたものまで、あるいはそれらが混ざりあうなど多様です。

一般的には、よく利用される構造化プログラミング言語の要素を含むプログラミング言語の特徴は次のようなものです。

  1. 代入 (a=b)

  2. 条件分岐 (if-else)

  3. 繰り返し (for, while, loop)

  4. 関数化 (def, func)

この他に、文字列操作変数の型外部ライブラリの呼び出し、などについてプログラミング言語毎に把握することが、多くの場合必要になります。

まず一つC言語などの代表的なプログラミング言語を習得しながら、各プログラミング言語で、どのようにこれらの処理が行なわれるか、調べて試すことで、様々なプログラミング言語を使いこなすことができるようになります。

この他に最近流行りの、RustやScalaのようなパラダイムの少し異なるプログラミング言語を習得するためには、背景にある関数型プログラミング言語や、オブジェクト指向言語といったパラダイムについての知識も必要になるでしょう。

以上