Flask-AskでAlexaのカスタムスキルを作成

はじめに

昨年末にAmazon Echo Dotを購入したので、カスタムスキルを作ろうと思い、その方法を備忘としてまとめます。

Alexaのカスタムスキルは以前作ったことがあり、そのときに使用したFlask-Askを使用して実装したいと思います。

Flask-Askをインストールする際の注意点はこちらの記事にまとめています。

kittagon.hateblo.jp

環境構築

DockerでPython環境の構築

今回は作ったプログラムと環境をHerokuにデプロイするので、コンテナをそのままデプロイ出来るようDockerで環境を作ります。

まずは簡単に以下のようなDockerfileを作ります。

FROM python:3.7.1-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && apk add git vim

以下コマンドでDockerコンテナを起動します。

$ docker build . -t alexa_env
$ docker run -it -v $PWD:/app -p 8000:8000 alexa_env /bin/ash

今回はalpineのDocker imageを使用しているので、/bin/bashではなくbin/ashであることに注意。

qiita.com

docs.docker.jp

Dockerコンテナに入ったら入ったら、flaskgunicornインストールします。

/app $ pip install flask gunicorn

動作確認のためのapp.pyを作成します。

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

gunicornを起動します。

/app $ gunicorn app:app -b 0.0.0.0:8000

ブラウザからhttp://localhost:8000を開いて"Hello World!"が表示されればOKです。

Herokuにデプロイ

続いてDockerfileを編集してHerokuにデプロイ出来る環境を作っていきます。
事前にコンテナ内でpip freeze > requirements.txtを実行しrequirement.txtを作成しておきます。
また前回の記事にて実施したFlask-Askのパッケージのローカルへのダウンロードと編集したパッケージを/src配下に準備するとともに、build-base,libffi-dev,openssl-devのインストールを追記しておきます。

最終的なDockerfileは以下の通り。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

以前の記事に書いたように、以下コマンドでherokuにデプロイします。

$ heroku login # herokuにログイン
$ heroku container:login # Heroku 上の Container Registry へログイン
$ heroku create alexacustomskil # Heorkuアプリの作成
$ heroku container:push web --app yourappname
$ heroku container:release web --app yourappname

これでHerokuにデプロイされ、デプロイされたURLにアクセスし先ほどと同様"hello World!"が表示されればOKです。

スキル作成

いよいよここからはカスタムスキルを作成していきます。
基本的にはこちらの開発ブログに掲載されているチュートリアルに従って実装していきます。

Flask-Ask: A New Python Framework for Rapid Alexa Skills Kit Development : Alexa Blogs

こちらのチュートリアルでは、数字が3つ読み上げられるのでその数字を覚えて逆から読むというゲームを作っています。

Alexa Developer Console上での設定と、Flask-Askでのサーバ側の処理実装が必要になります。

Amazon Developper Consoleで設定

チュートリアルとは少し画面構成が違いますが、Console上で設定するものは以下のものです。

  • スキルの作成
    • スキルの名前を設定 「アレクサ、〇〇を開いて」のときの名前
    • スキルのインテントを設定 インテントとはユーザーがアレクサに対して応答する言葉のカテゴリのようなもの そのユーザー応答が何を意味するのかを定義します。
      今回登録するのは以下の2つ
      • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
      • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義
    • それぞれのインテントのスロットを設定 インテントにて定義する発話の中で使用する、変数の器のようなもの 今回のAnswerIntentでは発話の中で数字を読み上げるので、固定値で発話パターンを定義出来ないので、変数としてスロットを定義したうえで発話を定義します。

まずはAlexa Developer ConsoleにAmazonアカウントでログインします。
開発者アカウントにするために追加で情報入力が必要な際は入力します。

https://developer.amazon.com/alexa/console/ask

開発者コンソールが開いたらスキルの作成をクリックします。

f:id:ti_taka:20190203184434p:plain

新しいスキルの登録画面で、カスタムスキルを作成します。

f:id:ti_taka:20190203184439p:plain f:id:ti_taka:20190203213541p:plain

必要情報を入力すると、スキル編集画面にきます。

f:id:ti_taka:20190203184452p:plain

まずはインテントを登録します。

f:id:ti_taka:20190203184452p:plain

今回は上で説明したとおり、以下の2つを登録します。

  • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
  • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義

まずはYesIntentを登録します。

f:id:ti_taka:20190203184457p:plain

また、YesIntentのサンプルを登録し、このIntentがどういった発話パターンを取りうるのかAlexaに知ってもらいます。

f:id:ti_taka:20190203184502p:plain

次にAnswerIntentを登録します。

f:id:ti_taka:20190203184509p:plain

AnserIntentについては、発話パターンのサンプルを登録する前に、発話パターンの中で使用されるスロット(変数、器のようなもの)を登録します。

f:id:ti_taka:20190203184515p:plain

今回は3つの数字のスロットを使用しますので、それぞれfirst、second、thirdの3個を登録します。 スロットのタイプはAMAZON.numberとします。

f:id:ti_taka:20190203184522p:plain

次にこのスロットを使用してAnswerIntentの発話パターンを登録します。 今回は以下の2パターンを登録しました。

f:id:ti_taka:20190203184526p:plain

次に、このスキルを呼び出した際にそれを処理するサーバーのURLを設定します。 先程デプロイしたherokuのURLを設定します。

f:id:ti_taka:20190203184530p:plain

最後に今回のスキルを呼び出すためのスキル名を設定します。 設定を保存し、ビルドします。

f:id:ti_taka:20190203184536p:plain

これで開発コンソールでの設定は完了です。

左メニューのJSONエディター上は以下のような表示になります。
このJSONをそのまま編集してもOKです。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "メモリーゲーム",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "YesIntent",
                    "slots": [],
                    "samples": [
                        "うん",
                        "はい",
                        "もちろん",
                        "OKです",
                        "OK",
                        "了解"
                    ]
                },
                {
                    "name": "AnswerIntent",
                    "slots": [
                        {
                            "name": "first",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "second",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "third",
                            "type": "AMAZON.NUMBER"
                        }
                    ],
                    "samples": [
                        "{first} と {second} と {third}",
                        "{first} {second} {third}"
                    ]
                }
            ],
            "types": []
        }
    }
}

Flask-ASKで実装

サーバ側の処理はFlask-Askで実装していきます。

Welcome to Flask-Ask — Flask-Ask documentation

チュートリアルの通り、app.pyを以下のように書き換えます。

import logging
from random import randint
from flask import Flask, render_template
from flask_ask import Ask, statement, question, session

app = Flask(__name__)
ask = Ask(app, "/")
logging.getLogger("flask_ask").setLevel(logging.DEBUG)

@ask.launch
``def new_game():
    welcome_msg = render_template('welcome')
    return question(welcome_msg)

@ask.intent("YesIntent")
def next_round():
    numbers = [randint(0, 9) for _ in range(3)]
    round_msg = render_template('round', numbers=numbers)
    session.attributes['numbers'] = numbers[::-1]  # reverse
    return question(round_msg)

@ask.intent("AnswerIntent", convert={'first': int, 'second': int, 'third': int})
def answer(first, second, third):
    winning_numbers = session.attributes['numbers']
    if [first, second, third] == winning_numbers:
        msg = render_template('win')
    else:
        msg = render_template('lose')
    return statement(msg)

if __name__ == '__main__':
    app.run(debug=True)

templateファイルの中にはAlexaが話すセリフを記載します。

welcome: メモリーゲームへようこそ! これから5つの数字を読み上げるので、アタナはその数字を逆の順番で答えてください。準備はいいですか?

round: 今回の数字は {{ numbers|join(", ") }} です。反対の順番で読み上げてください。

win: 正解です!

lose: 不正解です。

サーバー側の処理はこれでOKなので、再度Herokuにデプロイします。

そして開発用アカウントに紐づけたEcho端末や、アレクサシミュレーターで「アレクサ、メモリーゲームを開いて」と話しかけてゲームがスタートすれば成功です。

が、返事が返ってきません。

Herokuのログを確認すると以下のようなエラーが出ていました。

2019-01-24T15:02:16.836079+00:00 app[web.1]: File "/usr/local/lib/python3.7/site-packages/OpenSSL/crypto.py", line 740, in _subjectAltNameString
2019-01-24T15:02:16.836081+00:00 app[web.1]: method = _lib.X509V3_EXT_get(self._extension)
2019-01-24T15:02:16.836082+00:00 app[web.1]: AttributeError: module 'lib' has no attribute 'X509V3_EXT_get'

調べてみるとpyOpenSSLを再インストールする必要があるようです。

electricsheep.hatenadiary.jp

記事を参考にDockerfileを以下のように修正します。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz && \
    pip uninstall --yes pyOpenSSL && \
    yes | pip install pyOpenSSL
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

再度Herokuにデプロイすると無事動作するようになりました。

まとめ

前回の続きで、Flask-Askを使用してAlexaのカスタムスキルを作成してみました。
Flaskを使って簡単に実装できるので、アイデア次第でいろんなスキルが作れそうです。

Dockerのalpine上 Flask-Askをpipでインストールしようとするとエラー

はじめに

年末にEcho dotを購入したので、Alexaのカスタムスキルを作ろうと思いました。

Alexaのカスタムスキルは以前作成したことがあったのでその際に使用したFlask_Askを使って作成しようと思い取り掛かりました。

ところがFlask_Askはしばらくメンテナンスされていないようで、pipでインストールする際にエラーが出てしまったのでその対処を備忘としてまとめます。

pip installでエラー

今回はDocker上に環境を作っており、Python3.7.2-alpine3.8のDockerimageを使用しました。

Docker Hub

早速開発しようとFlask_Askをpipでインストールしようとしたらエラーになりました。

$ pip install flask_ask
Collecting flask_ask
  Using cached https://files.pythonhosted.org/packages/6a/f5/d4709ae94584a0b1541e9b52b2d25a8a1bdb6e2da9d6870f23fdd0523a30/Flask-Ask-0.9.8.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-install-la1z83pq/flask-ask/setup.py", line 8, in <module>
        from pip.req import parse_requirements
    ModuleNotFoundError: No module named 'pip.req'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-la1z83pq/flask-ask/

'pip.req'が無いと言われています。
いろいろ調べてみると既知のエラーらしく、コミュニティでも話題になっていました。
どうやらsetup.pyを編集すれば解消するようです。

stackoverflow.com

pip downloadでダウンロードしてからオフラインでインストール

setup.pyを編集するためには、まずはソースをダウンロードしなくてはなりません。

qiita.com

こちらを参考に以下の手順で実施しました。

まずはsrcディレクトリを作成し、そこにパッケージをダウンロードします。

$ mkdir src
$ pip download -d src --no-binary :all: flask_ask
Collecting flask_ask
  Downloading https://files.pythonhosted.org/packages/6a/f5/d4709ae94584a0b1541e9b52b2d25a8a1bdb6e2da9d6870f23fdd0523a30/Flask-Ask-0.9.8.tar.gz (40kB)
    100% |████████████████████████████████| 40kB 2.8MB/s
  Saved ./src/Flask-Ask-0.9.8.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-download-on3tlt2h/flask-ask/setup.py", line 8, in <module>
        from pip.req import parse_requirements
    ModuleNotFoundError: No module named 'pip.req'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-download-on3tlt2h/flask-ask/

$ ls -l src/
total 40
-rw-r--r--    1 root     root         40142 Jan  8 07:03 Flask-Ask-0.9.8.tar.gz

何やらまたsetup.pyでエラーが出ていますが、ちゃんとダウンロード出来ているようです。

setup.pyの編集

ダウンロードしたtar.gzファイルを解凍し、setup.pyを編集します。

$ cd src/
$ tar -zxvf Flask-Ask-0.9.8.tar.gz
$ ls -l
total 40
drwxr-xr-x   15 501      wheel          480 Jan  8 07:05 Flask-Ask-0.9.8
-rw-r--r--    1 root     root         40142 Jan  8 07:03 Flask-Ask-0.9.8.tar.gz
$ cd Flask-Ask-0.9.8
$ vim setup.py

setup.pyを以下のように編集します。

"""
Flask-Ask
-------------

Easy Alexa Skills Kit integration for Flask
"""
from setuptools import setup
# from pip.req import parse_requirements
try: # for pip >= 10
    from pip._internal.req import parse_requirements
except ImportError: # for pip <= 9.0.3
    from pip.req import parse_requirements

setup(
    name='Flask-Ask',
    version='0.9.8',
    url='https://github.com/johnwheeler/flask-ask',
    license='Apache 2.0',
    author='John Wheeler',
    author_email='john@johnwheeler.org',
    description='Rapid Alexa Skills Kit Development for Amazon Echo Devices in Python',
    long_description=__doc__,
    packages=['flask_ask'],
    zip_safe=False,
    include_package_data=True,
    platforms='any',
    install_requires=[
        str(item.req) for item in
        parse_requirements('requirements.txt', session=False)
    ],
    test_requires=[
        'mock',
        'requests'
    ],
    test_suite='tests',
    classifiers=[
        'License :: OSI Approved :: Apache Software License',
        'Framework :: Flask',
        'Programming Language :: Python',
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'Operating System :: OS Independent',
        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
        'Topic :: Software Development :: Libraries :: Python Modules'
    ]
)

setup.pyを編集したパッケージフォルダを再度tarで固めます。

$ mv Flask-Ask-0.9.8.tar.gz Flask-Ask-0.9.8_bk.tar.gz
$ tar -zcvf Flask-Ask-0.9.8.tar.gz Flask-Ask-0.9.8
$ ls -l
total 80
drwxr-xr-x   15 501      wheel          480 Jan  8 07:06 Flask-Ask-0.9.8
-rw-r--r--    1 root     root         40284 Jan  8 07:08 Flask-Ask-0.9.8.tar.gz
-rw-r--r--    1 root     root         40142 Jan  8 07:03 Flask-Ask-0.9.8_bk.tar.gz

alpineにgccのインストール

pipでインストールを実行したところ、何やら別のエラーが出ました。

$ pip install Flask-Ask-0.9.8.tar.gz
Processing ./Flask-Ask-0.9.8.tar.gz
Collecting aniso8601==1.2.0 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/5b/fb/251a0dd2f4710e60664ddd8bd3485bd8362530f47af9e88f4061fe589ebf/aniso8601-1.2.0.tar.gz (59kB)
    100% |████████████████████████████████| 61kB 2.2MB/s
Collecting Flask==0.12.1 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/f4/43/fb2d5fb1d10e1d0402dd57836cf9a78b7f69c8b5f76a04b6e6113d0d7c5a/Flask-0.12.1-py2.py3-none-any.whl (82kB)
    100% |████████████████████████████████| 92kB 5.1MB/s
Collecting pyOpenSSL==17.0.0 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/8c/b7/0048adbf09ad93f571e70fb3100e696d4711cdcbfe38161690d716a5ed35/pyOpenSSL-17.0.0-py2.py3-none-any.whl (51kB)
    100% |████████████████████████████████| 61kB 3.1MB/s
Collecting PyYAML==3.12 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/4a/85/db5a2df477072b2902b0eb892feb37d88ac635d36245a72a6a69b23b383a/PyYAML-3.12.tar.gz (253kB)
    100% |████████████████████████████████| 256kB 7.0MB/s
Collecting six==1.11.0 (from Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/67/4b/141a581104b1f6397bfa78ac9d43d8ad29a7ca43ea90a2d863fe3056e86a/six-1.11.0-py2.py3-none-any.whl
Collecting python-dateutil (from aniso8601==1.2.0->Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/74/68/d87d9b36af36f44254a8d512cbfc48369103a3b9e474be9bdfe536abfc45/python_dateutil-2.7.5-py2.py3-none-any.whl (225kB)
    100% |████████████████████████████████| 235kB 5.4MB/s
Requirement already satisfied: click>=2.0 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (7.0)
Requirement already satisfied: Jinja2>=2.4 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (2.10)
Requirement already satisfied: itsdangerous>=0.21 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (1.1.0)
Requirement already satisfied: Werkzeug>=0.7 in /usr/local/lib/python3.7/site-packages (from Flask==0.12.1->Flask-Ask==0.9.8) (0.14.1)
Collecting cryptography>=1.7 (from pyOpenSSL==17.0.0->Flask-Ask==0.9.8)
  Downloading https://files.pythonhosted.org/packages/f3/39/d3904df7c56f8654691c4ae1bdb270c1c9220d6da79bd3b1fbad91afd0e1/cryptography-2.4.2.tar.gz (468kB)
    100% |████████████████████████████████| 471kB 6.3MB/s
  Installing build dependencies ... error
  Complete output from command /usr/local/bin/python -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-x0_m8e40 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple -- setuptools>=18.5 wheel "cffi>=1.7,!=1.11.3; python_implementation != 'PyPy'":
  Collecting setuptools>=18.5
    Downloading https://files.pythonhosted.org/packages/37/06/754589caf971b0d2d48f151c2586f62902d93dc908e2fd9b9b9f6aa3c9dd/setuptools-40.6.3-py2.py3-none-any.whl (573kB)
  Collecting wheel
    Downloading https://files.pythonhosted.org/packages/ff/47/1dfa4795e24fd6f93d5d58602dd716c3f101cfd5a77cd9acbe519b44a0a9/wheel-0.32.3-py2.py3-none-any.whl
  Collecting cffi!=1.11.3,>=1.7
    Downloading https://files.pythonhosted.org/packages/e7/a7/4cd50e57cc6f436f1cc3a7e8fa700ff9b8b4d471620629074913e3735fb2/cffi-1.11.5.tar.gz (438kB)
      Complete output from command python setup.py egg_info:

          No working compiler found, or bogus compiler options passed to
          the compiler from Python's standard "distutils" module.  See
          the error messages above.  Likely, the problem is not related
          to CFFI but generic to the setup.py of any Python package that
          tries to compile C code.  (Hints: on OS/X 10.8, for errors about
          -mno-fused-madd see http://stackoverflow.com/questions/22313407/
          Otherwise, see https://wiki.python.org/moin/CompLangPython or
          the IRC channel #python on irc.freenode.net.)

      ----------------------------------------
  Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-zydf2ff8/cffi/

  ----------------------------------------
Command "/usr/local/bin/python -m pip install --ignore-installed --no-user --prefix /tmp/pip-build-env-x0_m8e40 --no-warn-script-location --no-binary :none: --only-binary :none: -i https://pypi.org/simple -- setuptools>=18.5 wheel "cffi>=1.7,!=1.11.3; python_implementation != 'PyPy'"" failed with error code 1 in None

コンパイラが見つからないよーというエラーが出ています。
調べると対処方法が見つかりました。

ja.stackoverflow.com

alpineにgccが含まれていない事が原因のようです。

早速alpine上でパッケージをインストールしてから再試行しますと、また別のエラーが、、、

$ apk add build-base libffi-dev
$ pip install Flask-Ask-0.9.8.tar.gz

〜〜〜〜略〜〜〜〜
  build/temp.linux-x86_64-3.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory
   #include <openssl/opensslv.h>
                                ^
  compilation terminated.
  error: command 'gcc' failed with exit status 1

  ----------------------------------------
  Failed building wheel for cryptography
〜〜〜〜略〜〜〜〜
    build/temp.linux-x86_64-3.7/_openssl.c:498:30: fatal error: openssl/opensslv.h: No such file or directory
     #include <openssl/opensslv.h>
                                  ^
    compilation terminated.
    error: command 'gcc' failed with exit status 1

    ----------------------------------------
Command "/usr/local/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-z1_hjgoi/cryptography/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-rfu9q4ne/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /tmp/pip-install-z1_hjgoi/cryptography/

今度はopensslが無い!と怒られてしまっています。インストールしてから再度pipでインストール実行します。

$ apk add openssl-dev
$ pip install Flask-Ask-0.9.8.tar.gz
〜〜〜〜略〜〜〜〜
Successfully built Flask-Ask cryptography
Installing collected packages: cryptography, pyOpenSSL, PyYAML, Flask-Ask
Successfully installed Flask-Ask-0.9.8 PyYAML-3.12 cryptography-2.4.2 pyOpenSSL-17.0.0

無事インストール出来ました。

$ python
Python 3.7.1 (default, Dec 21 2018, 03:21:42)
[GCC 6.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from flask_ask import Ask, statement, question, session
>>>
>>>

ちゃんとimportも出来るようになりました。
これでFlask_Askを使用できるようになったので、これからカスタムスキルを開発していきたいと思います。

locustでLINEメッセージ受信サーバに負荷試験

はじめに

業務でLINE Messeging APIを使用しているのですが、先日突然メッセージ受信サーバ(LINEからのWebhookを受け付けるサーバ)に対して、負荷試験を実施してほしいと先輩に依頼されました。
負荷試験ツールではjMeterが有名ですがノウハウがなく、もっと簡単に実施できるものが無いか調べたところ、 locustというPythonで記述できるツールがあることを知りました。
その内容を、自分用メモレベルですが備忘としてまとめます。

locustのインストール

公式サイト

Locust - A modern load testing framework

LocustはPython3.6までにしか対応していないので、Python3.6系の環境をあらかじめpyenvなどで準備しておきます。

Locust supports Python 2.7, 3.4, 3.5, and 3.6.

Macにてインストールする場合、libevが必要なので先にインストールしておきます。

$ brew install libev
$ pip install locustio

基本的な使い方は上記公式サイトや以下サイトを参照しました。

シナリオ作成

LINEから送られるWebhookを模してJSONのhttpヘッダーにx-line-signatureパラメータを追加して送っています。
リクエスト先はLine developerで設定しているcallbackURLのパスを指定しています。(今回は/callback
メッセージIDは、受け付けるサーバ側(今回負荷をかけるサーバ)でユニークなものでないと処理できない仕様になっていたようなので、タイムスタンプを設定して送っています。
Messaging API - LINE Developers

from locust import HttpLocust, TaskSet, task
import base64
import hashlib
import hmac
import datetime
import json

channel_secret = '  channnel secret   '

class UserBehavior(TaskSet):
    @task(1)
    def index(self):
        payload = {
                    "events": 
                    [
                        {
                            "replyToken": "0f377",
                            "type": "message",
                            "timestamp": 1462629479859,
                            "source": {
                                "type": "user",
                                "userId": "Ue636d0*****************"
                            },
                            "message": {
                                "id": str(int(datetime.datetime.now().timestamp()*1000000)),
                                "type": "text",
                                "text": "性能試験実施中"
                            }
                        }
                    ],
                    "destination":"U6415f13f***********"
                }
        hash = hmac.new(channel_secret.encode('utf-8'),json.dumps(payload).encode('utf-8'), hashlib.sha256).digest()
        signature = base64.b64encode(hash)
        headers = {'content-type': 'application/json', 'x-line-signature': signature}
        r = self.client.post("/callback", data=json.dumps(payload), headers=headers, catch_response=True)
        print(r)

class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    min_wait = 1000
    max_wait = 1000

以下コマンドで起動します。

$ locust -H http://"負荷をかけるサーバURL"

画面が起動して

  • Number of users to simulate:
    何クライアント作成するか
  • Hatch rate:
    クライアントの作成スピード(毎秒)

を入力するとリクエストが生成され、負荷試験できました!

Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その3〜〜ログイン認証追加〜〜

はじめに

今回は、前回作成したフロントのUIがVue.js、バックエンドのAPIFlaskのWebアプリケーションに対してFlask-Loginを使用してログイン機能を作成します。
Flask-Login

↓前回
kittagon.hateblo.jp

↓前々回 kittagon.hateblo.jp

まずはFlask-Loginとログインフォームの作成に使用するFlask-WTFを以下コマンドでインストールします。

$ pip install flask-login flask_wtf

ユーザーモデルの追加

まずはログインユーザーとなるユーザーモデルをmodels.pyに記述します。
公式サイトにも以下のように記載されており、UserクラスはUserMixinを継承する形で記述すると簡単なようです。

To make implementing a user class easier, you can inherit from UserMixin, which provides default implementations for all of these properties and methods. (It’s not required, though.)

また、ログインフォームはとあえず簡単にflask_wtfを使いLoginFormクラスを作成しておきます。

# backend/models.py


from flask_login import UserMixin
from flask_wtf import FlaskForm
from wtforms import TextField, PasswordField, validators
from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from backend import db

class LoginForm(FlaskForm):
    email = TextField('email', validators=[validators.Required()])
    password = PasswordField('Password', validators=[validators.Required()])
    def __str__(self):
        return '''
<form action="/-/login" method="POST">
<p>%s: %s</p>
<p>%s: %s</p>
%s
<p><input type="submit" name="submit" /></p>
</form>
''' % (self.email.label, self.email,
       self.password.label, self.password,
       self.csrf_token)

class User(UserMixin, db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True, nullable=False)
    email = db.Column(db.String(255), unique=True, nullable=False)
    _password = db.Column('password', db.String(255), nullable=False)
    active = db.Column(db.Boolean(), default=True)

    def _get_password(self):
        return self._password
    def _set_password(self, password):
        if password:
            password = password.strip()
        self._password = generate_password_hash(password)
    password_descriptor = property(_get_password, _set_password)
    password = synonym('_password', descriptor=password_descriptor)

    # フォームで送信されたパスワードの検証
    def check_password(self, password):
        password = password.strip()
        if not password:
            return False
        return check_password_hash(self.password, password)

    # 認証処理
    @classmethod
    def auth(cls, query, email, password):
        user = query(cls).filter(cls.email==email).first()
        if user is None:
            return None, False
        return user, user.check_password(password)

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)

    def to_dict(self):
        return dict(
            id=self.id,
            title=self.title,
            text=self.text
        )

    def __repr__(self):
        return '<Entry id={id} title={title!r}>'.format(
            id=self.id, title=self.title)

def init():
    db.create_all()

ログイン処理の追加

次にFlask側の処理の中に実際のログイン処理を記述していきます。
__init__.pyFlask-Loginの初期化
view.pyでログイン画面のルーティングとログイン処理を実装します。

このあたりを参考。
Flask-Loginの使い方 - Qiita
Python Flaskでつくる LDAPログインページ - AAA Blog
Flaskで認証 - Kensuke Kousaka's Blog

config.py

# backend/config.py

import os
class BaseConfig(object):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///backend.db'
    # cookieを暗号化する秘密鍵
    SECRET_KEY = os.urandom(24)
    # csrf token用秘密鍵
    WTF_CSRF_SECRET_KEY = os.urandom(24)

init.py

Flask-loginを使うとcurrent_userlogin_userlogout_userという変数が払い出されて払い出されていい感じにログイン状態の確認ができます。
また、後述するview.pyに記載するルーティングを-というプレフィックスをつけて取り込んでいます。

# backend/__init__.py

from flask import Flask, request, redirect, url_for
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user, login_user, logout_user, login_required # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app)

######ここから追記######

login_manager = LoginManager()
login_manager.init_app(app)

from backend.models import User

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)

# 未認証の際のリダイレクト先を設定
@login_manager.unauthorized_handler
def unauthorized_callback():
    return redirect(url_for('app.login'))

############# route ##############

from backend.view import view
app.register_blueprint(view, url_prefix="/-")

######ここまで追記######

from backend.api import api
app.register_blueprint(api, url_prefix="/api")
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
@login_required # ここを追記
def catch_all(path):
    if app.debug:
        return requests.get('http://localhost:8080/{}'.format(path)).text
    return render_template("index.html")

view.py

from flask import Blueprint, redirect, request
from flask_login import login_user, logout_user

from backend import db
from backend.models import User, LoginForm

view = Blueprint('app', __name__,
                    template_folder='templates',
                    static_folder='templates/static')

@view.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user, authenticated = User.auth(db.session.query, form.email.data, form.password.data)
        if authenticated:
            login_user(user, remember=True)
            print('Login successfully.')
            return redirect('/tasks')
        else:
            print('Login failed.')
    return str(form)

@view.route('/logout', methods=['POST'])
def logout():
    logout_user()
    return redirect('/login')

ログアウトボタンの追加

ログインした後のタスク画面にログアウトボタンを追加します。
以下のボタンをTasks.vueの中に記述します。
場所はどこでも結構です。

// frontend/src/component/Tasks.vue

<b-form v-if="show" action="/-/logout" method="post" >
  <b-button type="submit">ログアウト</b-button>
</b-form>

ログインユーザーの作成

まだログインユーザーを作成する画面を作っていないので、
Pythonインタラクティブモードからユーザーを作成し、動作確認をします。

$ python
>>> from backend.models import User, db
>>> User.query.all()
[]
>>> user = User(name='test_user', email='test@example.com')
>>> user._set_password('password')
>>> user
<User (transient 4334868296)>
>>> db.session.add(entry)
>>> db.session.commit()
>>> User.query.all()
[<User 1>]

http://localhost:5000/-/loginを開いてログイン画面が表示され、
'test@example.com'と'password'でログインできればOKです。
また、ログインしていない状態でhttp://localhost:5000/tasksを表示しようとすると
ログイン画面にリダイレクトされます。
ログアウトボタンでログアウトできます。

ログイン画面修正

最初に作成したのは最低限の要素のみ記述したので、もう少しきれいなレイアウトに変更します。
現状は↓のようにフォームを文字列で定義しておき、それを表示しているだけだったので、
画面をテンプレートから呼び出せるよう変更します。

    def __str__(self):
        return '''
<form action="/-/login" method="POST">
<p>%s: %s</p>
<p>%s: %s</p>
%s
<p><input type="submit" name="submit" /></p>
</form>
''' % (self.email.label, self.email,
       self.password.label, self.password,
       self.csrf_token)

テンプレートの作成には、Vue.js側で作成している画面と揃えるために、Flask-Bootstrapを使用します。

Flask-Bootstrap

まずは以下コマンドでインストールします。

$ pip install flask-bootstrap

__init__.pyでflask-bootstrapの初期化をします。

# backend/__init__.py

from flask import Flask, request, redirect, url_for
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, current_user, login_user, logout_user
from flask_bootstrap import Bootstrap # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app)

bootstrap = Bootstrap(app) # ここを追記

#############以下略#############

view.pyのログイン画面表示を修正します。
テンプレートの呼び出しに加えて、actionをFlaskテンプレートの変数としてurl_forを渡すように修正しています。

# backend/view.py

from flask import Blueprint, redirect, request, render_template, url_for # render_template, url_forを追加

#############略#############

@view.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if form.validate_on_submit():
        user, authenticated = User.auth(db.session.query, form.email.data, form.password.data)
        if authenticated:
            login_user(user, remember=True)
            print('Login successfully.')
            return redirect('/tasks')
        else:
            print('Login failed.')
    # return str(form)
    ## ここを修正
    return render_template('login.html', form=form, action=url_for('app.login'))
#############略#############

models.pyからstr要素を削除します。
また、Loginform内に新たにsubmit要素を定義します。

# backend/models.py

from flask_login import UserMixin
from flask_wtf import FlaskForm
from wtforms import TextField, PasswordField, validators, SubmitField # SubmitFieldの追加
from sqlalchemy.orm import synonym
from werkzeug import check_password_hash, generate_password_hash

from backend import db

class LoginForm(FlaskForm):
    email = TextField('email', validators=[validators.Required()])
    password = PasswordField('Password', validators=[validators.Required()])
    submit = SubmitField('ログイン') # submit追加
    # def __str__(self):の削除

次にログインテンプレートを作成します。
テンプレートフォルダはview.pyにて以下のように定義されています。

view = Blueprint('app', __name__,
                    template_folder='templates',
                    static_folder='templates/static')

view.pyと同階層にtemplatesフォルダを作成します。

app_dir
 ┣manage.py
 ┣frontend/
 ┗backend/
   ┣__init__.py
   ┣api.py
   ┣view.py
   ┣models.py
   ┣config.py
   ┗templates/

templatesフォルダ内にログイン画面用テンプレートを作成します。

<!-- backend/templates/login.html-->
{% import "bootstrap/wtf.html" as wtf %}
{% extends "bootstrap/base.html" %}
{% block title %}This is a login page{% endblock %}
{% block content %}
  <div class="container">
    {{ wtf.quick_form(form, action=action, method="post") }}
  </div>
{% endblock %}

またまた画面デザインは適当ですが、Bootstrapにより良い感じのフォームができました。

まとめ

これで認証までも含めたWebアプリができました。
基本的な機能はほぼ実装することができたので、今後はこれをベースにいろんな機能を実装していきたいと思います。

参考サイト

vue-cliのnpm run devでwebpack-dev-serverのエラー

はじめに

vue-cliを使ってVue.jsの開発をしていたら、あるときからnpm run dev実行時に↓のようなエラーが出てしまい開発用サーバが立ち上がらなくなりました。
(なんの操作をしていたときか忘れてしまいました。。)

$ npm run dev
> frontend@1.0.0 dev /Users/hogehoge/Develop/practice-flask-vue/frontend
> webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

The CLI moved into a separate package: webpack-cli
Please install 'webpack-cli' in addition to webpack itself to use the CLI
-> When using npm: npm i -D webpack-cli
-> When using yarn: yarn add -D webpack-cli
internal/modules/cjs/loader.js:583
    throw err;
    ^

Error: Cannot find module 'webpack-cli/bin/config-yargs'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
    at Function.Module._load (internal/modules/cjs/loader.js:507:25)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:20:18)
    at Object.<anonymous> (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:84:1)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! frontend@1.0.0 dev: `webpack-dev-server --inline --progress --config build/webpack.dev.conf.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the frontend@1.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/hogehoge/.npm/_logs/2018-11-19T11_12_55_011Z-debug.log

webpack-cliのインストール

しばらくパッケージをアップデートしていないので、なぜ今のタイミングで

The CLI moved into a separate package: webpack-cli
Please install 'webpack-cli' in addition to webpack itself to use the CLI
-> When using npm: npm i -D webpack-cli
-> When using yarn: yarn add -D webpack-cli

が出るのか不明ですが、ひとまず指示どおりにインストールします。

$ npm i -D webpack-cli
npm WARN url-loader@1.1.0 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN webpack-dev-middleware@3.4.0 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN webpack-dev-server@3.1.10 requires a peer of webpack@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN webpack-cli@3.1.2 requires a peer of webpack@^4.x.x but none is installed. You must install peer dependencies yourself.

+ webpack-cli@3.1.2
added 25 packages from 7 contributors and audited 34200 packages in 37.642s
found 0 vulnerabilities

インストールが完了しました。
webpack@^4.0.0をインストールせよと言われてしまいます。

$ npm run dev

> frontend@1.0.0 dev /Users/hogehoge/Develop/practice-flask-vue/frontend
> webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

TypeError: Cannot destructure property `compile` of 'undefined' or 'null'.
    at addHooks (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/lib/Server.js:114:49)
    at new Server (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/lib/Server.js:127:5)
    at startDevServer (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:355:14)
    at processOptions (/Users/hogehoge/Develop/practice-flask-vue/frontend/node_modules/webpack-dev-server/bin/webpack-dev-server.js:309:5)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at startup (internal/bootstrap/node.js:266:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

やっぱりまだだめみたいです。

webpack-dev-serverのダウングレード

TypeError: Cannot destructure property `compile` of 'undefined' or 'null'.

そこでこちらのエラーについて更にいろいろと調べてみると、同じようなissueがありました。

TypeError: Cannot read property 'compile' of undefined · Issue #1334 · webpack/webpack-dev-server · GitHub

1 - Upgrade webpack to v4, you must change the webpack.config.js file for compatibility. or
2 - Downgrade webpack-dev-server to exactly v3.0.0 as @Nufeen said.

やはり上で突っ込まれているように、v4以上にアップグレードするか、v3にダウングレードすれば直るようです。
webpackの知識はほぼ無く、webpack.config.jsを編集するのもハードルが高そうだったので今回はダウングレードして対応することとしました。

$ npm install -D webpack-dev-server@3.0.0
npm WARN webpack-cli@3.1.2 requires a peer of webpack@^4.x.x but none is installed. You must install peer dependencies yourself.
npm WARN webpack-dev-server@3.0.0 requires a peer of webpack@^4.0.0-beta.1 but none is installed. You must install peer dependencies yourself.

+ webpack-dev-server@3.0.0
added 37 packages from 16 contributors, removed 28 packages, updated 10 packages and audited 35284 packages in 30.206s
found 1 high severity vulnerability
  run `npm audit fix` to fix them, or `npm audit` for details

インストール出来たので再度試してみます。

$ npm un dev


 DONE  Compiled successfully in 26990ms                                                    8:59:34 PM

 I  Your application is running here: http://localhost:8080

無事開発用サーバが起動しました。

スティード400を復活!!〜キャブ洗浄編〜

はじめに

最近またバイクが乗りたくなってきたので、
高校時代に乗っていたスティード400を修理することにしました。
大学3年ぐらいの時に軽く乗って以来実家の倉庫に眠りっぱなしなので、かれこれ8年ぶり?ぐらいになります。
今年のお盆に作業して、車のバッテリーと接続してエンジンをかけることは出来たのでまとめます。

洗浄方法

自分ではバイクの整備はよく分からないので詳しい友人に聞きながら作業を進めました。
まずはキャブをオーバーホールしなきゃならないだろうなーと思っていたのですが、 中のパーツ交換するではなく洗浄するだけならもっと簡単な方法があるとの事で、その方法を実践しました。

なにやらスティードはタンクからのガソリンを一旦シート下のポンプに流し、そのポンプからキャブへ燃料を送っている構造になっており、
ポンプのチューブからクリーナーを噴射すれば簡単にキャブ内の洗浄が出来るらしいです。
今回は友人お勧めのワコーズのクリーナーを使いました。

いざ実践!

こちらが筆者のスティード400です。
高校の頃に乗っていたままで実家の車庫に眠っていました。
バッテリーも上がっておりエンジンもかかりません。
f:id:ti_taka:20181117212708j:plain

まずはシートを外します。ここはシート下のボルトを外すだけなので簡単ですね。
f:id:ti_taka:20181117212730j:plain

次はタンクを外しにかかりますが、スティードの構造上燃料コックを外さないとタンクが外せません。
燃料コックの奥にあるネジを回し、外していきます。必ずOFFにしてガソリンの流れを止めておくように。
(かなり奥にあるので鈍らないように注意!!)
f:id:ti_taka:20181117212758j:plain

こんな感じで外せます。
f:id:ti_taka:20181117212820j:plain

燃料コックが外せると、タンクを少し上に浮かせることができるので、チューブに繋がってるボルトを外します。
f:id:ti_taka:20181117212845j:plainf:id:ti_taka:20181117212856j:plainf:id:ti_taka:20181117212906j:plain

また、タンク端のネジを外します。
f:id:ti_taka:20181117212934j:plain

外せました!
f:id:ti_taka:20181117213023j:plainf:id:ti_taka:20181117213035j:plain

次に、まずキャブのドレンボルトを外しておきます。
f:id:ti_taka:20181117213101j:plain

ここを外しておくことでポンプ側からクリーナーを入れた時に汚れが外に流れ出てきます。
キャブの中にガソリンを残したまま長期間放置してしまうとキャブの中に汚れが溜まり、
クリーナーを入れる前からドロっとした汚れが出てくるので、
あらかじめ紙などを敷いておきましょう。
右側と左側のキャブそれぞれにあるのでどちらも忘れずに開けましょう。
洗浄途中の写真ですが、こんな感じに外れます。
f:id:ti_taka:20181117213131j:plain

次にようやくポンプ側のチューブを取り外しにかかります。
ガソリンを送るポンプはシート下にあります。
f:id:ti_taka:20181117213153j:plain

こちらのチューブがキャブに繋がっているのでこれを外します。
f:id:ti_taka:20181117213236j:plainf:id:ti_taka:20181117213241j:plain

ここが外せればあとはこちらのクリーナーを入れるだけです。

  • 注入口をしっかりと塞いで5秒ほど噴射して注入
  • 汚れがだらだらとドレンボルトから流れ出てくるのでしばらく待つ

というのを繰り返します。
f:id:ti_taka:20181117213301j:plain

するとこんな感じに徐々に出てくる汚れが薄くなっていきます。
f:id:ti_taka:20181117213323j:plain

最終的にはこれぐらいに薄まるまで上記の手順を繰り返します。
f:id:ti_taka:20181117213346j:plainf:id:ti_taka:20181117213400j:plain

これで概ねキャブの洗浄は完了です。
後はこのままポンプやチューブをつなぎ直してエンジンをかけてみます。
ドレンボルトから流れ出た分以外のクリーナーが少しキャブの中に残ってしまいますが、ガソリンで流されるので問題ないそうです。

とりあえず車のバッテリーと接続してエンジンをかけてみます。
車側のプラス端子とバイク側のバッテリープラス端子を接続し、車側のマイナス端子はどこか導電体の金属の箇所に接続し、アースします。
f:id:ti_taka:20181117213437j:plainf:id:ti_taka:20181117213450j:plainf:id:ti_taka:20181117213501j:plain

これでセルを回すと無事エンジンがかかりました!!
かなり久しぶりにエンジンの音を聞いたので感動です!!

終わりに

今回はとりあえず簡易的な方法でキャブを洗浄し、エンジンをかけるところまで実践しました。
が、実際に乗るためには整備しなくてはならない箇所がまだまだ残っています。 ぱっと思いつく限りでこんなに。。

  • エアクリーナーのゴムパッキン交換
  • エンジンオイル交換
  • チェーン交換
  • サイドカバー交換
  • シフトレバー交換
  • フロントフォークのオイル漏れ修理
  • シート交換
  • ウィンカー交換
  • マフラー交換
  • ディスクブレーキ専用液補充
  • タイヤ、ホイール交換
  • ラジエーター修理
  • 全体、サビ落とし、塗装

来年、遅くとも再来年にはもう一度乗れるよう細々と修理していきます!

Vue.jsとFlaskでフルスタックなWebアプリの開発環境を構築 その2〜〜投稿画面作成〜〜

はじめに

前回作ったこちらの環境を用いて、タスク管理アプリ的なものを作成していきたいと思います。

kittagon.hateblo.jp

タスクのモデルを作成(Flask)

まずはこちらのサイトを参考に、タスクのクラスを作成し一通りFlask側のみで動作するところまでを作成いたします。
2. Flaskチュートリアル

前回からのファイル構成変更点

その前に、前回作成した構成を若干修正します。

前回は下記の構成だったかと思いますが、今後の扱いやすさを考慮しファイル名とその中の記載内容を変更します。

app_dir
 ┣appserver.py → manage.pyにファイル名変更
 ┣frontend/
 ┗backend/
   ┣api.py
   ┣application.py → __init__.pyにファイル名変更
   ┗config.py

それぞれファイル名を変更したものの中身は以下です。

# __init__.py(←application.py)

from flask import Flask
from flask_cors import CORS

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

from backend.api import api
app.register_blueprint(api, url_prefix="/api")
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
    if app.debug:
        return requests.get('http://localhost:8080/{}'.format(path)).text
    return render_template("index.html")
# maange.py (←appserver.py)

from backend import app

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

sqliteを使用する設定

チュートリアルに倣い、sqliteはSQLAlchemyで操作します。
まずは以下コマンドでインストールします。

Flask-SQLAlchemyのインストール

$ pip install Flask-SQLAlchemy

次にconfig.pyを編集します.

# config.py

import os
class BaseConfig(object):
    DEBUG = True
    ## 以下を追記
    SQLALCHEMY_DATABASE_URI = 'sqlite:///backend.db'
    # cookieを暗号化する秘密鍵
    SECRET_KEY = os.urandom(24)
# __init__py

from flask import Flask
from flask_cors import CORS
import requests
from flask_sqlalchemy import SQLAlchemy # ここを追記

app = Flask('FLASK-VUE',
            static_folder = "./dist/static",
            template_folder = "./dist")
app.config.from_object('backend.config.BaseConfig')

db = SQLAlchemy(app) # ここを追記

from backend.api import api
app.register_blueprint(api, url_prefix="/api")
cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
    if app.debug:
        return requests.get('http://localhost:8080/{}'.format(path)).text
    return render_template("index.html")

models.pyの作成

チュートリアルに倣い、以下のように記述します。

# backend/models.py

from backend import db

class Task(db.Model):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Text)
    text = db.Column(db.Text)

    def to_dict(self):
    return dict(
        id=self.id,
        title=self.title,
        text=self.text
    )

    def __repr__(self):
        return '<Task id={id} title={title!r}>'.format(
            id=self.id, title=self.title)

def init():
    db.create_all()

コマンドラインからデータベースを初期化します。

$ python
>>> from backend.models import init
>>> init()

テータを登録してみます。

$ python
>>> from backend.models import Task, db
>>> Task.query.all()
[]
>>> task = Task(title='title', text='text')
>>> db.session.add(task)
>>> db.session.commit()
>>> Task.query.all()
[<Task id=1 title=u'title'>]

>>> task = Task.query.get(1)
>>> task
<Task id=1 title=u'title'>
>>> task.title = 'hello'
>>> task.text = 'Hello world'
>>> db.session.add(task)
>>> db.session.commit()
>>> Task.query.all()
[<Task id=1 title=u'hello'>]

APIの作成(Flask)

まずはFlask側で登録のAPIを作成します。
基本は先程のチュートリアルを参考にしつつ、以下のように記述します。

# backend/api.py

from flask import Blueprint, jsonify, request, url_for, make_response
from random import *
from flask_cors import CORS

from backend import app, db
from backend.models import Task

api = Blueprint('api', __name__)

@api.route('/hello/<string:name>/')
def say_hello(name):
    response = { 'msg': "Hello {}".format(name) }
    return jsonify(response)

@api.route('/random')
def random_number():
    response = {
        'randomNumber': randint(1, 100)
    }
    return jsonify(response)

@api.route('/get', methods=['GET'])
def get_taks():
    taks = Task.query.order_by(Task.id.desc()).all()
    taks_dict = [task.to_dict() for task in taks]
    return jsonify(taks_dict)

@api.route('/add', methods=['POST'])
def add_task():
    task = Task(
            title=request.form['title'],
            text=request.form['text']
            )
    db.session.add(task)
    db.session.commit()
    task = Task.query.order_by(Task.id.desc()).first()
    id = str(task.id)
    r = make_response(id)
    return r

@api.route('/delete', methods=['POST'])
def delete_task():
    id=request.form['id']
    task = Task.query.get(id)
    db.session.delete(task)
    db.session.commit()
    r = make_response(id)
    return r

http://127.0.0.1:5000/api/getにアクセスして、以下のようにレスポンスが返却されればOKです。

[
    {
        "id": 1,
        "text": "Hello world",
        "title": "hello"
    }
]

登録画面の作成(Vue.js)

ルーティングの追加

続いて、Vue.jsのSPA側で、タスクの登録と閲覧をする画面を作成します。

$ cd frontend/src/component
$ cp Home.vue Tasks.vue

また、SPA側のルーティングにもタスク画面を表示するように記述します。

// frontend/src/router/index.js


/////// 省略 ///////
const routerOptions = [
  { path: '/', component: 'Home' },
  { path: '/about', component: 'About' },
  { path: '/tasks', component: 'Tasks' },//ここを追記
  { path: '*', component: 'NotFound' }
]
/////// 省略 ///////

bootstrap-vueの導入

登録画面を作成するにあたってはbootstrap-vueを使用したいと思います。

公式サイトに記載されている通り、以下コマンドでインストールします。

$ npm install jquery
$ npm install bootstrap-vue

また、こちらも記載されている通り、frontend/src/main.jsfrontend/src/App.vueに以下の記述を記載します。

// frontend/src/main.js

import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue' //ここを追記
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  render: h => h(App)
})

Vue.use(BootstrapVue) //ここを追記
// frontend/src/App.vue

/////// 省略 ///////

<script>
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
export default {
  name: 'App'
}
</script>

/////// 省略 ///////

画面の作成

そして、Tasks.vueにて先程作成したバックエンド側のAPIを叩き、UIに表示するviewを作成します。
デザインは適当です笑。

// frontend/src/component/Task.vue

<template>
  <div class="container">
    <div align="center">
      <div class="col-sm-8 col-md-6 col-lg-6">
      <p>タスクの登録</p>
        <b-form v-if="show">
          <b-form-group label="Title:"
                        label-for="title">
            <b-form-input id="title"
                          type="text"
                          required
                          placeholder="Enter title">
            </b-form-input>
          </b-form-group>
          <b-form-group label="Text:"
                        label-for="title">
            <b-form-textarea id="text"
                             placeholder="Enter something"
                             :rows="3"
                             :max-rows="6">
            </b-form-textarea>
          </b-form-group>
          <div align="center">
            <div class="col-sm-4 col-md-2 col-lg-2">
              <b-button block @click="addTask" variant="success">Add</b-button>
            </div>
          </div>
        </b-form>
      </div>
    </div>

    <b-list-group v-for="(task, index) in tasks" :key='index'>
      <b-list-group-item>
        {{ task.title }}<br>
        {{ task.text }}
        <b-button v-bind:src="task.id" block @click="deleteTasks(index, task.id)">削除</b-button>
      </b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data () {
    return {
      randomNumber: 0,
      tasks: [],
      show: true
    }
  },
  methods: {
    getTasks () {
      const path = 'http://localhost:5000/api/get'
      axios.get(path)
        .then(response => {
          this.tasks = response.data
        })
        .catch(error => {
          console.log(error)
        })
    },
    addTask () {
      const path = 'http://localhost:5000/api/add'
      var title = document.getElementById('title')
      var text = document.getElementById('text')
      let params = new URLSearchParams()
      params.append('title', title.value)
      params.append('text', text.value)
      var titleValue = title.value
      var textValue = text.value
      title.value = ''
      text.value = ''
      axios.post(path, params)
        .then(response => {
          var id = response.data
          var task = {'id': id, 'text': textValue, 'title': titleValue}
          this.tasks.unshift(task)
          console.log(response)
        })
        .catch(error => {
          console.log(error)
        })
    },
    deleteTasks (taskIndex, taskId) {
      console.log(taskIndex)
      console.log(taskId)
      const path = 'http://localhost:5000/api/delete'
      let params = new URLSearchParams()
      params.append('id', taskId)
      this.tasks.splice(taskIndex, 1)
      axios.post(path, params)
        .then(response => {
          console.log(response)
        })
        .catch(error => {
          console.log(error)
        })
    }
  },
  created () {
    this.getTasks()
  }
}
</script>

http://127.0.0.1:5000/tasksを開けば以下のように登録と削除ができる画面が動作すると思います。
f:id:ti_taka:20181113213852g:plain

あまりタスク管理っぽくないですが。笑

終わりに

これで、バックエンドとフロントエンドの役割が明確なWebアプリを作成出来ました。
あとはバックエンドのFlaskでAPIを作成し、フロントエンドのVue.jsでUIを描画すればいろんなWebアプリが作れるようになります。
次回はこのWebアプリにログイン機能をつけたいと思います。

↓続き kittagon.hateblo.jp

参考サイト