Photoruction工事中!

Photoructionの開発ブログです!

web hookを利用したAPIのテストにngrok(pyengrok)を使いローカルサーバーにレスポンスを送る

はじめに

こんにちは、AIエンジニアの山下です。

これまではweb hookを利用したAPIの解析結果を確認・テストするのにPostmanwebhook.siteを使用していました。

少し確認する程度ならこれでもよかったのですが、何度もテストを行いたい場合などは毎回ブラウザーを開いて確認するとなると結構面倒。

そこで、python+Flask(Webアプリケーションフレームワーク)で簡易的なローカルサーバーを構築し、ngrokというツール(ローカルサーバーと外部ネットワークを繋ぐツール)を使用し、ブラウザーやターミナルを開かず、jupyter notebook内でAPIへのリクエストの送信からローカルサーバーでのレスポンスのデータの保存までできることを目指しました。

(*ネットワークやwebアプリケーションに慣れていないので細かい部分で認識が間違っていたらすみませんmm)

*OSはMac

前提として

  • ngrokにユーザー登録が済んでいる
  • 今回想定しているのはwebAPIにリクエストを送り、そのレスポンス(解析結果)をweb hookとして送る(指定したURLにPOSTリクエストする)という構成
  • jupyter notebookでpythonモジュール(app.py)を読み込んだ上でリクエスト送信→APIからwebhookをローカルサーバーに送る→ローカルサーバーでレスポンス内容をjsonに変換したものを保存するまでの処理を行いたい
  • ngrokにはngrokコマンドをインストールして使えるが、処理をjupyter notebook内で完結させたいため、Pythonラッパーのpyngrokというライブラリーを使用
  • python内で”pip install flask pyengrok”が事前に必要

流れ

  1. 作成したpythonファイル(モジュール)とその説明
  2. 1.を読み込むjupyter notebookの説明

app.py

  • 作成したモジュールです、この後に内容を説明していきます。
import json
import logging
import os
import threading
from logging import config

from flask import Flask, jsonify, request
from pyngrok import ngrok
from werkzeug.serving import run_simple

from config.logging_config import LOGGING_CONFIG

class CreateApp():
    ana_js = None

    def __init__(self, on_colab: bool = False) -> None:
        os.environ["FLASK_DEBUG"] = "development"

        # Open a ngrok tunnel to the HTTP server
        port = 8888  # ポート番号
        public_url = ngrok.connect(port).public_url
        self.public_url = public_url.replace('http', 'https')  # httpsに変換
        print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(
            self.public_url, port))

        app = Flask(__name__)
        app.debug = True
        app.config["SECRET_KEY"] = "secret key"  # 要変更
        # Update any base URLs to use the public ngrok URL
        app.config["BASE_URL"] = self.public_url
        app.config["ENV"] = "development"
        app.config['JSON_AS_ASCII'] = False  # JSON日本語文字化け対策
        # .. Update inbound traffic via APIs to use the public-facing ngrok URL

        # logging設定
        config.dictConfig(LOGGING_CONFIG)
        logger = logging.getLogger()

        # Start the Flask server in a new thread
        if on_colab is False:
            threading.Thread(
                target=lambda: run_simple('localhost', port, app)).start()
        else:
            # on Colab, you can use the following code instead of the above
            # line
            threading.Thread(
                target=app.run, kwargs={"use_reloader": False}).start()

        # Define Flask routes
        @app.route("/", methods=['GET'])
        def index():
            return "Web App with Python Flask!"

        @app.route('/receiver', methods=['POST'])
        def get_json():
            js = request.get_json()
            if js is not None:  # JSONが送られてきた場合、DB & ローカルに保存
                logger.error('ERROR')
                self.ana_js = js  # class変数に保存
                with open(f'./result/{js["metadata"]["pdf_name"]}.json',
                          'w') as f:
                    json.dump(js, f, indent=4)
            return jsonify(js)

    def receive(self) -> dict:
        while True:
            if self.ana_js is not None:
                return self.ana_js
            else:
                continue

pyngrokを使用し、HTTPサーバーを開く

  • port は何でも良さそうでdefaultだと5000になります
  • public_urlがngrokが作成したトンネルのURLになります
# Open a ngrok tunnel to the HTTP server
port = 8888  # ポート番号
public_url = ngrok.connect(port).public_url
self.public_url = public_url.replace('http', 'https')  # httpsに変換
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(
    self.public_url, port))

Flask側の設定

  • app.config[”BASE_URL”]に上記の”public_url”を指定する
  • loggingも設定しましたが、本筋ではないので詳細は省きます
app = Flask(__name__)
app.debug = True
app.config["SECRET_KEY"] = "secret key"  # 要変更
# Update any base URLs to use the public ngrok URL
app.config["BASE_URL"] = self.public_url
app.config["ENV"] = "development"
app.config['JSON_AS_ASCII'] = False  # JSON日本語文字化け対策
# .. Update inbound traffic via APIs to use the public-facing ngrok URL

# logging設定
config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger()

python HTTPの設定

  • このあたりちゃんと理解できていませんが二つの処理を同時に実行させるために並列処理を行いFlaskサーバーをスレッドを使用し、実行しているようです
# Start the Flask server in a new thread
if on_colab is False:
    threading.Thread(
        target=lambda: run_simple('localhost', port, app)).start()
else:
    # on Colab, you can use the following code instead of the above
    # line
    threading.Thread(
        target=app.run, kwargs={"use_reloader": False}).start()

Flaskの処理とルーティング

  • こちらなくても良いのですが、サーバーが起動しているかどうかをブラウザーlocalhost:8888にアクセスし確認するために処理を入れました。
# Define Flask routes
@app.route("/", methods=['GET'])
def index():
    return "Web App with Python Flask!"
  • こちら、ローカルサーバーで受け取ったレスポンスをjsonに変換し、ローカルの指定したディレクトリに保存する処理になります。(また、そのjsonをクラス変数に代入する処理も行ってます、この変数に代入されたデータは後ほど使います)
@app.route('/receiver', methods=['POST'])
def get_json():
    js = request.get_json()
    if js is not None:  # JSONが送られてきた場合、DB & ローカルに保存
        logger.error('ERROR')
        self.ana_js = js  # class変数に保存
        with open(f'./result/{js["metadata"]["pdf_name"]}.json',
                  'w') as f:
            json.dump(js, f, indent=4)
    return jsonify(js)

クラス内関数を定義

  • APIにリクエストを送り、レスポンスが返ってくるまでにタイムラグがあり、ローカルサーバーにレスポンスが送られるまで実行し続ける関数になります
  • 何のための関数か分かりづらいかもしれませんが、後のjupyter notebook内の全ての処理(セル)を一度の実行で行いため実装しています
def receive(self) -> dict:
    while True:
        if self.ana_js is not None:
            return self.ana_js
        else:
            continue

JupyterNotebook

  • 1番目のセル
    • 必要なライブラリを読み込んでます(app→先に作成したモジュール)

  • 2番目のセル

    • 1行目:ローカルサーバーの起動
    • 2行目:public_urlの取得(パブリックngrokURL)
    • 3行目:アクティブなトンネルをリストで取得

  • 3番目のセル

    • 各パラメーターを指定し、APIにPOSTリクエストし、APIの処理が実行されローカルサーバーにweb hookを送るまでの処理

    *url, api_key, pdf_path, dataの内容はダミー

  • 4番目のセル

    • app.pyのCreateAppクラス内関数receiveを実行
    • このセルが実行完了(ローカルサーバーがレスポンスを受信、かつ、レスポンスjsonの保存)次第、次のセルが実行される

    *この処理を入れずに次のセルを実行するとローカルサーバーがレスポンスを受信する前にトンネルを閉じてしまう場合がある

  • 5番目のセル

    • 開いているトンネル閉じる

    *jupyter notebookを再起動するだけでトンネルは閉じられる

jupyter notebook コード

import json

import requests
from pyngrok import ngrok

from app import CreateApp
# local server を起動
ca = CreateApp()
public_url = ca.public_url
tunnels = ngrok.get_tunnels()
# APIにデータを送信
url = 'https://OOOO.com/OOO' # ダミー
api_key = 'api_key'. # ダミー
webhook_url = public_url + '/receiver'

pdf_path = 'OOOO/OOOO/OOO.pdf'
metadata = {'pdf_name': '0000.pdf'}
# postする
data = {
    'data:': 'data', webhook_url: 'webhook_url', metadata: {'hoge': 'test'}}
js_str = json.dumps(data).encode('utf-8')
response_local = requests.post(
    url, data=js_str, headers={'x-api-key': api_key})
print(f'response_local: {response_local}')
ca.receive().keys()
print(f'Before tunnels:\n{ngrok.get_tunnels()}')
# Close all open ngrok tunnels
_ = [ngrok.disconnect(tnnl.public_url) for tnnl in tunnels]
print(f'After tunnels:\n{ngrok.get_tunnels()}')

最後に

webやネットワークに慣れてないこともあり、言語化するのに苦労しました。

(*今回のものはあくまで、開発テスト確認用になります)

次はFlaskのローカルサーバーにログイン機能を実装してみたいと思います。

株式会社フォトラクションでは一緒に働く仲間を募集しています

corporate.photoruction.com www.wantedly.com