9/7~9/10に行われたPyConJP 2017に参加し、『Djangoフレームワークのユーザーモデルと認証』というタイトルで話してきました。
Djangoフレームワークのユーザーモデルと認証
Djangoの認証まわりを触る前に知っておくとよさそうなことを説明しました。
スライドは、slideshareにアップロードしています。
Djangoのモデルクラスにフィールドやメソッドを定義しておいて、さらに属性値を代入していて、この値を取得するコードを書く場合。
メソッドのほうはgetattrでとればいいのだけど、同様にDjangoのフィールドのほうはget_fieldでフィールドを取得しないといけない。
メソッドから取得する方法に失敗したら、Djangoのフィールドとみなしてget_fieldメソッドを呼ぶようなコードを書いていた。
しかし、Django1.10以降だとModel.フィールド名でアクセスすると、DeferredAttributeが返され、hasattrがTrueになるため、はまったりしていた。
test.py:
def main(): import django django.setup() from django.db import models class MyModel(models.Model): class Meta: app_label = "__main__" def method1(self): pass method1.attr2 = "egg" # この値をとりたい field1 = models.CharField(max_length=10) field1.attr1 = "spam" # この値も取りたい # メソッドに指定された属性の取得 print("method: ", MyModel.method1) print("hasattr: ", hasattr(MyModel, "method1")) print("getattr: ", getattr(MyModel.method1, "attr2", "invalid")) # Model.フィールド名だとDeferredAttributeが返されるので同じやり方ではダメ print("field: ", MyModel.field1) print("hasattr: ", hasattr(MyModel, "field1")) print("getattr: ", getattr(MyModel.field1, "attr1", "invalid")) # _meta.get_fieldを使うのが正解 print("attr1: ", MyModel._meta.get_field("field1").attr1) if __name__ == '__main__': main()
Django1.9だとDeferredAttributeは返されずエラー(当時はここからフォールバックして_meta.get_fieldでアクセスするコードを書いていた)
$ DJANGO_SETTINGS_MODULE=project.settings venv-dj19/bin/python test.py method: <function main.<locals>.MyModel.method1 at 0x7fe61a6b90d0> hasattr: True getattr: egg Traceback (most recent call last): File "test.py", line 32, in <module> main() File "test.py", line 23, in main print("field: ", MyModel.field1) AttributeError: type object 'MyModel' has no attribute 'field1'
Django1.10以降はエラーにならなくなった。挙動の違いでハマった。
$ DJANGO_SETTINGS_MODULE=project.settings venv-dj110/bin/python test.py method: <function main.<locals>.MyModel.method1 at 0x7f6ac886bea0> hasattr: True getattr: egg field: <django.db.models.query_utils.DeferredAttribute object at 0x7f6ac8de19e8> hasattr: True getattr: invalid attr1: spam
Django1.11
$ DJANGO_SETTINGS_MODULE=project.settings venv-dj111/bin/python test.py method: <function main.<locals>.MyModel.method1 at 0x7fd012e3e1e0> hasattr: True getattr: egg field: <django.db.models.query_utils.DeferredAttribute object at 0x7fd01342cbe0> hasattr: True getattr: invalid attr1: spam
www.ospn.jp 聞きに来てくださった方はありがとうございました。
今年も北海道の人たちに会えたのでよかったです。また来年。
www.slideshare.net github.com
2017/10/12 追記: API.AIのサービス名はDialogflowに変わりました。
API.AIのContextsとWebhookでChatOps環境を作ってみます。 https://docs.api.ai/docs/concept-contextsdocs.api.ai 今回は、SlackのChatbotとやりとりし、「管理者モード開始」から「管理者モード終了」までのやりとりの間に操作コマンドを実行できるようにします。
インテントは『管理者モード開始』、『管理者モード終了』、『stats』の3つを用意します。
『管理者モード開始』は、Output contextに「administrator」、User saysに「管理者モード開始」、アクションに「enter_administration」、パラメータとしてpasswordを@sys.anyエンティティで必須入力としておきます。また、パスワード検証をWebhook側で行うため、「Use Webhook」もチェックしておきます。
『管理者モード終了』は、Input contextに「administrator」、Output contextに「administrator」で、Output側はLife spanを0に設定します。Life spanを0にしたコンテキストを出力すると、既存のコンテキストから外れるようです。TextResponseで「管理者モードを終了します。」を返すように設定します。終了時はコンテキストを外す処理だけなので、Webhookは使いません。
『stats』は、操作コマンド用に使います。Input contextに「administrator」、Output contextに「administrator」で、OutputのLifespanは5としておきます。アクションは「stats」を設定しておきます。Webhookで処理するため、「Use Webhook」もチェックしておきます。
今回は操作コマンドとして、botのホストされている環境のメモリ使用量を表示してみます。コマンド名はstatsとしておきます。 WebフレームワークはFlask、また追加のモジュールでpsutilを使います。 Pythonのバージョンは3.6です。
from flask import Flask, request, jsonify import psutil app = Flask(__name__) SECRET_PASSWORD = 'hoge' @app.route("/", methods=['POST']) def webhook(): req = request.get_json(silent=True, force=True) resp = jsonify(process_request(req)) return resp def process_request(request_data): if "result" not in request_data: return {} action = request_data['result']['action'] if action == 'stats': return stats() elif action == 'enter_administration': return enter_administration(request_data) return {} def enter_administration(request_data): # get context contexts = request_data['result']['contexts'] for context in contexts: if context['name'] == 'administrator': administrator_context = context break else: return {} password = administrator_context['parameters']['password'] if password != SECRET_PASSWORD: return { 'speech': "パスワードが間違っています。", 'contextOut': [{'name': 'administrator', 'lifespan': 0}]} message = "管理者モードを開始します。" return { "data": {"slack": {"text": message}}, "displayText": message, "speech": message} def stats(): mem = psutil.virtual_memory() total_gb = mem.total / (1024 ** 3) used_gb = mem.used / (1024 ** 3) message = "メモリ使用量: {:01.1f}GB / {:01.1f}GB".format(used_gb, total_gb) return { "data": {"slack": {"text": message}}, "displayText": message, "speech": message} if __name__ == "__main__": app.run()
起動はpythonコマンドにソースコードのファイルを指定します。psutilを使っているのでWindowsでも動作します。
$ python main.py
起動すると、localhost:5000でlistenされます。
前回と同様に、ngrokで外部から接続できるようにして設定します。
$ ngrok http 5000
fulfillmentのWebhookにURLを設定してSAVEしておきます。
Slackでテスト用に追加されたapiai_botにDMで話しかけてみます。
正常動作していれば、botから応答があります。
コンテキストを使って状態を変えられるのは便利ですね。またコンテキストにパラメータを保持できるのも何かに使えそうです。
今回はパスワード検証部分はハッシュ化などはしていませんが、ワンタイムトークンを発行したりすれば、実用上も問題無さそうですかね。
2017/10/12 追記: API.AIのサービス名はDialogflowに変わりました。
Slackのbotなどを作るときに、API.AIを使うと面白いか便利かもねという話を聞いたので試しています。
API.AIの概要は公式ドキュメントの説明がわかりやすいのでそちらを読むのをオススメします。
自分が思った要点はこのあたり:
音声入力からのテキスト変換もapi.aiがやってたみたいですが、Deprecatedになってて、今はOSの機能か外部サービスを使うのを推奨しているみたいです。
自分の理解ではSlack、API.AI、webhookの関係を図にするとこんな感じ。ドキュメントのほうにも図があるので、そちらも参照されたし。
2016年にGoogleに買収されて、GCPとの連携が強化されていってるのかな。現状、利用は無料。
とりあえず、Slackからの発言をfulfillmentサービスのwebhookで受け取って、レスポンスを返すというのを試してみることにします。
今回は、Contextは使わず単純な文章の加工だけをやってみます。
この記事以外のサンプルコードだと、Webhookのドキュメントにあるものも参考にするとよさそう。
GitHub - dialogflow/fulfillment-webhook-weather-python
左メニューのエージェント名が表示される部分のドロップダウンから「Create new agent」をクリックしてエージェント作成画面を表示。
エージェント名の入力と言語を日本語にしてSAVE。
エンティティは、ユーザー入力から得られる値の定義です。
左メニューのENTITIESのところにある「+」ボタンをクリックしてエンティティの追加画面を表示。 エンティティ名をfood、値を「お好み焼き」「たこ焼き」で入力してSAVE。 ここで定義するもの以外にシステム定義のものや自由入力も扱えます。
インテントは、ユーザー発言をどのように変換、取り扱うかのルールの定義です。
左メニューのINTENTのところにある「+」ボタンをクリックしてインテントの追加画面を表示。 UserSaysに「大阪でたこ焼き食べます」と入力してEnterキーを押すと、エンティティが認識されます。今回の場合、「大阪」の部分は @sys.geo-city 、「たこ焼き」の部分は作成しておいたエンティティの @food と判定されました。
@sys.geo-cityは組み込みのエンティティです。一覧はドキュメントに記載があります。
SAVEボタンをクリックして保存します。
右ペインの「Try it now」のテキストボックスに、「東京でたこ焼きを食べます」と入力してパラメータの変換を試すと、geo-cityは「東京」、foodは「たこ焼き」として認識されました。
「SHOW JSON」ボタンをクリックすると、後述するfulfillmentサービスで外部に送信されるJSONを確認できます。
今回はインテントで変換したパラメータをWebhookで処理します。
Webhook用のWebサーバーとしてPythonとFlaskで簡単なレスポンスを返すものを用意しました。Pythonのバージョンは3.6です。
main.py:
from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/", methods=['POST']) def webhook(): req = request.get_json(silent=True, force=True) resp = jsonify(process_request(req)) return resp def process_request(request_data): if "result" not in request_data: return {} food = request_data['result']['parameters']['food'] city = request_data['result']['parameters']['geo-city'] message = "場所: {}, 食べ物: {}".format(city, food) return { "data": {"slack": {"text": message}}, } if __name__ == "__main__": app.run()
パラメータで受け取った内容から定型文を生成して返すぐらいのものです。
起動はpythonコマンドにソースコードのファイルを指定します。
$ python main.py
起動すると、localhost:5000でlistenされます。
fulfillmentは外部サービスなどでレスポンスを返す機能です。先程用意したWebhookでレスポンスを返すように設定します。
webhookはapi.ai側から通信できる必要があります。どこかにデプロイしてもいいのですが、お試しなのでngrokを使いました。
ngrokコマンドが使える状態で、次のコマンドを実行すると、localhost:5000にngrok経由で外部からアクセスできるようになります。
$ ngrok http 5000
fulfillmentのWebhookにURLを設定してSAVEしておきます。
また、インテントの編集画面の下部に「Use Webhook」というチェックボックスがあるので、チェックしてSAVEしておきます。
Slackのbotとして動かすため、左メニューの「Integrations」からSlackのインテグレーションをONにします。
Slackへの接続ダイアログが表示されるので、任意のスラックグループを選びます。個人用のSlackを作って試すことをオススメします。
Slackのインテグレーションを有効にすると、Slack側にはapiai_botというアカウントが現れます。
Slackでテスト用に追加されたapiai_botにDMで話しかけてみます。
正常動作していれば、botから応答があります。
与えた文章から得たパラメータで応答されることを確認できました。
自然言語処理の部分を作り込むのは結構面倒なものですが、API.AIが肩代わりしてくれるのはうれしいですね。
API.AIのコンソールのUIがわかりづらい(どこがクリックできるのか、入力できるのかなど見た目でわからない、ラベルがない入力エリアなど)のですが、今後改善されることに期待したいです。
学習部分の調整は大変そうですが、いろいろ試していきたいです。
bpmappersのバージョン0.9を公開しました。
リポジトリはbeproud organization以下にありますが、権限を頂いて引き続きメンテナンスしています。
以前のバージョンでは、django.utilsにあったSortedDictとMultiValueDictをコピーして使っていたのですが、これを廃してOrderedDictとdefaultdictで動作するように改修しました。
また、テストコードはunittest+noseからpytestに変更しました。
古いDjangoのサポートを外して、1.8~1.11をサポートするようにしました。現状、Django 2.0で使えなくなるAPIを使っているところがあるので、次のバージョンでは直していきたいです。
django-ftpserverのバージョンを0.5.0に上げました。
pyftpdlibのFileSystemにDjangoのStorageAPIをつなげる機能を追加してみました。
StorageAPIとつなげると、面白そうだなーというのは前から考えてたんですが、StorageAPI側の機能が足りなくてラッパーが必須でいい解決方法が思い浮かばず放置になってました。
DjangoのStorageAPIはバージョンが上がるにつれて、ちょっとずつ機能が増えて、ようやく少しのラッピングでFileSystemの実装ができそうだったので、やってみた感じ。
django-storages経由でS3にFTPクライアントでつなげたり、django-gcloud-storage経由でGCSにつなげたりして遊んでました。
たのしい(^o^)