Vue.js + Bootstrap-vue でモーダルウィンドウとフラッシュメッセージの表示サンプル

忘れそうなのでメモしておきます。子が親のメソッドを呼び出したり親が子のメソッドを呼び出したりして分かりづらくなっているので後日もっと良い方法を検討したいと思います。

基本的にはBootstrap-vueの公式に書いてあった方法で実装していきます。
https://bootstrap-vue.js.org/docs

作成したファイルは以下の4つです。
簡単なメモアプリ上でメモを削除する動きを作ってみます。

↓メインのコンポーネント

// App.vue
<template>
  <div id="app">
    <Modal @delete="deleteMemo" ref="childModal"/>
    <Memos @confirm="deleteConfirm" ref="childMemos"/>
    <Message ref="childMessage"/>
  </div>
</template>

<script>
import Modal from './components/Modal.vue'
import Message from './components/Message.vue'
import Memos from './components/Memos.vue'

export default {
  name: 'app',
  components: {
    Modal,
    Memos,
    Message
  },
  methods: {
    deleteConfirm(i) {
      this.$refs.childModal.showModal(i)
    },
    deleteMemo(i) {
      this.$refs.childMemos.deleteMemo(i)
      this.$refs.childModal.hideModal(i)
      this.$refs.childMessage.showMessage(true)
    },
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

↓メモリストのコンポーネント
これは適当に書きました。

// Memos.vue
<template>
  <div class="container">
    <b-list-group>
      <b-list-group-item v-for="memo,i in memos"><b>{{ memo.title }}</b>{{ memo.text }}<b-button variant="danger" @click="deleteComfirm(i)">削除</b-button></b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
export default {
  name: 'Memos',
  data() {
    return {
      memos: [
        {"title": "title1", "text": "memomemoemomeo"},
        {"title": "title2", "text": "memomemomemomemo"},
        {"title": "title3", "text": "memomemomemomemo"},
        {"title": "title4", "text": "memomemomemomemo"},
        {"title": "title5", "text": "memomemomemomemo"},
        {"title": "title6", "text": "memomemomemomemo"},
        {"title": "title7", "text": "memomemomemomemo"},
        {"title": "title8", "text": "memomemomemomemo"},
        {"title": "title9", "text": "memomemomemomemo"},
      ]
    }
  },
  methods: {
    deleteComfirm(i) {
      this.$emit('confirm', i);
    },
    deleteMemo(i) {
      // 実際にはここに削除処理を記述
      this.memos.splice(i, 1);
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

↓モーダルのコンポーネント
https://bootstrap-vue.js.org/docs/components/modal/#using-show-hide-and-toggle-component-methods
こちらの
Using show(), hide(), and toggle() component methods
の方法を使って、モーダルの表示、非表示を操作しています。

// Modal.vue
<template>
  <b-modal ref="modal" size="xl" hide-footer hide-header>
    <div class="d-block text-center">
      <p>本当に削除しますか?</p>
    </div>
    <b-button class="delete-button" variant="success" block @click="hideModal()">いいえ</b-button>
    <b-button class="delete-button" variant="danger" block @click="deleteMemo()">はい</b-button>
  </b-modal>
</template>

<script>
export default {
  name: 'Modal',
  data() {
    return {
      deleteIndex: ""
    }
  },
  methods: {
    showModal(i){
      this.deleteIndex = i;
      this.$refs.modal.show()
    },
    hideModal(){
      this.deleteIndex = "";
      this.$refs.modal.hide()
    },
    deleteMemo(){
      this.$emit('delete', this.deleteIndex);
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

↓フラッシュメッセージのコンポーネント
https://bootstrap-vue.js.org/docs/components/alert/#auto-dismissing-alerts
Auto dismissing alerts
の実装サンプルを使い表示後2秒経過すると消えるようにしています。
また、transitionでアニメーションを入れています。
https://jp.vuejs.org/v2/guide/transitions.html

// Message.vue
<template>
  <transition name="message">
    <div v-if="messageActive" id="message-parent">
      <div id="message">
        <b-alert :show="dismissCountDown" v-bind:variant="variant" @dismiss-count-down="countDownChanged">
          {{ message }}
        </b-alert>
      </div>
     </div>
  </transition>
</template>
<script>
export default {
  name: 'Message',
  data() {
    return {
      // message表示用データ
      dismissSecs: 2,
      dismissCountDown: 0,
      message: '',
      variant:'',
      messageActive:false,
    }
  },
  methods: {
    countDownChanged (dismissCountDown) {
      this.dismissCountDown = dismissCountDown
      if (dismissCountDown == 0) {
        this.messageActive = false;
      }
    },
    showMessage: function (success) {
      this.message = success ? "削除しました" : "エラーが発生しました";
      this.variant = success ? "success" : "danger";
      this.dismissCountDown = this.dismissSecs;
      this.messageActive = true;
    },
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.message-enter-active, .message-leave-active {
  transition: opacity 0.5s;
}
.message-enter, .message-leave-to {
  opacity: 0;
}
#message-parent {
  width:100%;
}
#message {
  position: fixed;
  top: 5px;
  right: 0;
  left: 0;
  margin-left: auto;
  margin-right: auto;
  box-sizing:border-box;
  width:95%;
}
</style>

↓こんな表示になります。
f:id:ti_taka:20190409014407g:plain

処理の順番としては、

  1. Memos.vue中のdeleteComfirm(i)が発火
  2. App.vue中の@confirm="deleteConfirm" → deleteConfirm(i)が発火
  3. Modal.vue中のshowModal(i)が発火
  4. Modalの表示
  5. 「はい」を選択
  6. Modal.vue中のdeleteMemo()が発火
  7. App.vue中の@delete="deleteMemo" → deleteMemo(i)が発火
  8. Memos.vue中のdeleteMemo(i)が発火
  9. Modal.vue中のhideModal(i)が発火
  10. Message.vue中のshowMessage(true)が発火
  11. countDownChanged (dismissCountDown)にて1秒毎にカウントダウンし、設定秒経過したらメッセージを消す

という流れになっています。

とりあえず作りたいものは作れましたが、状態を管理するデータがいろんなコンポーネントに散在してしまっているので、今後はVuexなどを使って管理できるようにしたいと思います。

人間の時間の捉え方は結局今やりたい事、やらなくてはならない事がどれぐらいあるかに依存する

小さい頃から時間の感じ方の違いがなんで起きうるのか疑問に思っていました。
どうして楽しい時間は早く感じるのか、めんどくさい作業をしている時間は長く感じるのか。
小学校の時、国語の教科書に代謝時間という考え方が載っていたのを覚えています。
自分の体の持っている全エネルギーのうち単位時間でどの程度エネルギーを消費しているか、どの程度の心拍数があるか。
それが大きければ時間は早く感じ、小さければ時間は長く感じる
という理屈です。
(この辺りあまり覚えてませんがそんな定義だったと思います。)
なので、蟻にとっては一日もあっという間に過ぎるけれども、象にとっては長く感じると。
この考え方に、「確かにそうかもなぁ〜。」と思う反面、なんとなく納得しない部分もありました。
エネルギー消費が大きく、疲れる退屈な仕事でも時間は長く感じるし、消費が少なくても時間が短く感じることはある。
その違いはなんだろうかと思いました。

大人になってから、誰かから「1歳児にとっての一年間は一生分と同じ時間だが、50歳にとっての一年間はせいぜい人生の中の50分の1に過ぎない。」という話を聞きました。
x時間生きた人にとっての単位時間は人生の中での1/xでしか無い、それを積分していくと対数の関数になる、だから時間の感じ方は齢とともに短く感じるようになっていくんだなぁと思ってました。
と思ったら、既に「ジャネの法則」というものが、フランスの哲学者・ポール・ジャネにより発案されていたようです。

人間の五感は対数に変換されている

この記事を見て妙に納得したのを覚えています。

しかし、このジャネの法則は、あくまで過去を振り返っての感覚時間における法則であり、この先の1時間、1年間、更には自分の人生の長さをどう捉えるかの感覚とはまた違います。

仕事をし始めて、あらゆるプロジェクトに携わる中で、「これだけのプロジェクトをあと一年でやりきるのか、、あと一年しかないのか、、」などと思う事が増えたり、自身のキャリアを考えた時にも、「**年後には**が出来るようになっていたい!!その為には毎日勉強時間を確保したとしても***時間しかない。。全然時間ないなぁ〜。」と思う事が多くなりました。
また、子供ができた事で、「子供が**歳になるまでには家が欲しいなあ〜、その為には毎年**円貯金しなきゃー」と考えるようになりましたが、この先の数年間はとても短い時間のように感じられます。

こうしたことから、新たに以下の仮説を提唱したいと思います。
「人間の未来に向けた感覚時間は、この先の時間の中でやるべき、やりたいと考えている事柄の総量に反比例し、その内ある時間の中で実施し切れる(と想定される)量に比例する」

つまり、やりたいこと、やるべきことが多い人は、その中である時間の中で実施しきれる割合は相対的に少なくなります。
逆に、あまりタスクを抱えていない状態だと、時間は相対的に長く感じるようになります。
当たり前の事を言っているようですが、退屈な作業をしているときに時間が長く感じられるのは、作業のゴールイメージが無いか、やりたくもなく、やるべきことだとも認識出来ていないため、分母が極限まで0に近くなってしまうためかとも考えられ、なんとなく説明が付く気がします。

  1. 過去の時間を振り返る際の感覚時間は、現在自分が持っている記憶の量に反比例し、ある時間の中で増加した記憶の量に比例する。(ジャネの法則
  2. 未来時間を意識した際の感覚時間は、この先の時間の中でやるべき、やりたいと考えている事柄の総量に反比例し、その内ある時間の中で実施し切れる(と想定される)量に比例する

時間の捉え方が短くなったとか、長く感じる場合は、1.だけでなく、2.の要素も複合的絡み合っての結果なのかなと思います。
年を重ねて時間が無いと思うようになるのは、ある時間の中で得られる経験値が、自分の人生の中での占める割合が少しづつ減っていく為、だけでなく、いろんな考え方ができるようになったぶん、やりたいことが増えたため、または立場も変わっていく中でやるべきことが増えていったためという側面もあります。

そう考えると、時間が短く感じるのは「歳とったな〜。。」と嘆くためのネタになるだけでなく、この先の人生の中で自分はどういった事をやりたいのか、それに向けてどの程度のことが成し遂げられそうか、を意識するための丁度よい指標のような物になるのかもしれません。
長期的にやりたいこと、近い将来やるべきことなど、時にはそういったことから一旦頭を切り離し、ゆったりとした時間を過ごしたり、目の前の物事に取り組んで充実感を感じるというのが一番精神衛生上健康的な時間の過ごし方なのかなーと思いました。
常に、「時間が短い」と感じる場合は意識的にそういう時間を設けるのも必要かもしれません。

時間を意識しながらの方が効率的に動けることもありますが、いつも時間を意識するのも辛いですからね。

今日はそんな事を考えました。
明日からまた頑張っていこうと思います。

pythonでping監視 〜〜その2〜〜

前回、あるNWセグメント内のクライアントにpingを送って常時監視することを試みました。

kittagon.hateblo.jp

その別バージョンです。xargsで並列処理をしています。

# ping_xargs.py

import subprocess, requests, sys

def is_connectable(host):
    # ping = subprocess.Popen(["ping", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping = subprocess.Popen(["ping", "-W", "1", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

ipaddress = sys.argv[2]
num = sys.argv[1]
result = is_connectable(ipaddress)
print(ipaddress)
if result is not True:
    url = "https://my-line-bot-url.com/alert" + num
    requests.get(url)
$ while true; do sed -e 's/,/ /g' IPAdressTable.csv | xargs -L 1 -P20 python ping_xargs.py; done;

特に効率的になったわけではないですが。

しかも今回監視をしたいと思っていたNW設計を聞いたら一番上のL3SWではポート間のICMPを禁止しているらしい。。。
L3SWにぶら下がってるHUB間で全然疎通しないからなんでかなと思っていたら、そもそもpingで監視が出来るNW環境じゃなかった。。

pythonでping監視

はじめに

  • あるプライベートネットワークの中に存在するクライアントにpingを打ち、常時監視するツールが欲しかった。
  • メールでの通知とかも分かりづらいのでいろんな通知の方法が使いたかった(今回はLine Botで通知)。
  • 良さげなツールが無かったので自分でプログラムを書いて実現することにした

PythonPingを打つ

pythonではsubprocessを使えばpingを打てるらしいです。

qiita.com

この関数をお借りしてpingを打ちます。

import subprocess
def is_connectable(host):
    ping = subprocess.Popen(["ping", "-w", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

今回監視したい対象は、あるセグメントの中で固定IPを振られて存在しているクライアントが対象なので、クライアントのIPアドレスcsvファイルから読み込みます。

取得したIPアドレスのリストをforループで回して全てにpingを打ち、NGだった場合は通知を送ります。
(今回はLine Botに通知させたいのでBotサーバのURLにリクエストしています。)

1,192.168.101.1
2,192.168.101.2
3,192.168.101.3
4,192.168.101.4
:
:
import csv, subprocess, requests

def is_connectable(host):
    ping = subprocess.Popen(["ping", "-W", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

while True:
    f = open('IPAdressTable.csv', 'r')
    reader = csv.reader(f)
    for index, row in enumerate(reader):
        print(row)
        result = is_connectable(row[1])
        if result is not True:
            url = "https://my-line-bot-url.com/alert" + row[0]
            requests.get(url)
    f.close()

これで、まずは機能としては完成ですが、同セグメント内に存在する100個近くのクライアントにpingを打っていると、一周するのも時間がかかってしまい、あまり常時監視している感じがしなくなってしまいます。

そこで並列処理を導入して、複数スレッドで処理させようと思います。

joblibで並列処理

qiita.com

pythonの並列処理にはjoblibが便利みたいです。

ここを参考にループ処理の部分を書き換えます。

import csv, subprocess, requests
from joblib import Parallel, delayed

def is_connectable(host):
    ping = subprocess.Popen(["ping", "-W", "3", "-c", "1", host], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    return ping.returncode == 0

def send_connection_status(host, number):
    print(host)
    result = is_connectable(host)
    if result is not True:
        url = "https://my-line-bot-url.com/alert" + row[0]
        requests.get(url)

while True:
    f = open('IPAdressTable.csv', 'r')
    reader = csv.reader(f)
    header = next(reader)
    Parallel(n_jobs=-1)( [delayed(send_connection_status)(row[1], row[0]) for index, row in enumerate(reader)] )
    f.close()

これで複数スレッド立てて処理が出来るようです。
とりあえず実装しただけで細かいことはまだよくわかりませんが。

時間があるときにもう少し勉強したいと思います。

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:
    クライアントの作成スピード(毎秒)

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