Django1.10以降のDeferredAttributeとフィールドへのアクセス

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

参考

Model _meta API | Django documentation | Django

7/15に北海道のオープンソースカンファレンスでDjangoの紹介をしてきました。

www.ospn.jp 聞きに来てくださった方はありがとうございました。

今年も北海道の人たちに会えたのでよかったです。また来年。

スライドとサンプルコード

www.slideshare.net github.com

API.AIのコンテキストを使ってChatOps環境を作る

API.AIのContextsとWebhookでChatOps環境を作ってみます。 docs.api.ai 今回は、SlackのChatbotとやりとりし、「管理者モード開始」から「管理者モード終了」までのやりとりの間に操作コマンドを実行できるようにします。

インテントを用意する

インテントは『管理者モード開始』、『管理者モード終了』、『stats』の3つを用意します。

『管理者モード開始』は、Output contextに「administrator」、User saysに「管理者モード開始」、アクションに「enter_administration」、パラメータとしてpasswordを@sys.anyエンティティで必須入力としておきます。また、パスワード検証をWebhook側で行うため、「Use Webhook」もチェックしておきます。

f:id:nullpobug:20170612185733p:plain

『管理者モード終了』は、Input contextに「administrator」、Output contextに「administrator」で、Output側はLife spanを0に設定します。Life spanを0にしたコンテキストを出力すると、既存のコンテキストから外れるようです。TextResponseで「管理者モードを終了します。」を返すように設定します。終了時はコンテキストを外す処理だけなので、Webhookは使いません。

f:id:nullpobug:20170612185725p:plain

『stats』は、操作コマンド用に使います。Input contextに「administrator」、Output contextに「administrator」で、OutputのLifespanは5としておきます。アクションは「stats」を設定しておきます。Webhookで処理するため、「Use Webhook」もチェックしておきます。

f:id:nullpobug:20170612185716p:plain

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から応答があります。

f:id:nullpobug:20170612185713p:plain

感想

コンテキストを使って状態を変えられるのは便利ですね。またコンテキストにパラメータを保持できるのも何かに使えそうです。

今回はパスワード検証部分はハッシュ化などはしていませんが、ワンタイムトークンを発行したりすれば、実用上も問題無さそうですかね。

API.AIを試す

Slackのbotなどを作るときに、API.AIを使うと面白いか便利かもねという話を聞いたので試しています。

api.ai

API.AIってどういうものか

API.AIの概要は公式ドキュメントの説明がわかりやすいのでそちらを読むのをオススメします。

Introduction · API.AI

自分が思った要点はこのあたり:

  • 自然言語のテキスト入力を解析、パラメータに変換して外部のWebhookに流せる
    • 日本語にも対応している
    • 定型文で返すのならWebhookに流さなくてもできる感じ
  • SlackなどとのインテグレーションはAPI.AI側でやってくれる
  • Webhookでは簡単なJSONレスポンスを返すことで、api.ai側からインテグレーション先への応答を返してくれる
    • 音声読み上げとの連携もしやすい
  • 解析、変換の部分は機械学習エンジンが入っていて、学習データを与えてモデルを構築できる
  • 各種プログラミング言語、プラットフォーム向けのSDKが提供されている

音声入力からのテキスト変換もapi.aiがやってたみたいですが、Deprecatedになってて、今はOSの機能か外部サービスを使うのを推奨しているみたいです。

自分の理解ではSlack、API.AI、webhookの関係を図にするとこんな感じ。ドキュメントのほうにも図があるので、そちらも参照されたし。

f:id:nullpobug:20170606121048p:plain

2016年にGoogleに買収されて、GCPとの連携が強化されていってるのかな。現状、利用は無料。

試してみる

とりあえず、Slackからの発言をfulfillmentサービスのwebhookで受け取って、レスポンスを返すというのを試してみることにします。

今回は、Contextは使わず単純な文章の加工だけをやってみます。

この記事以外のサンプルコードだと、Webhookのドキュメントにあるものも参考にするとよさそう。

GitHub - api-ai/apiai-weather-webhook-sample

エージェントを登録する

左メニューのエージェント名が表示される部分のドロップダウンから「Create new agent」をクリックしてエージェント作成画面を表示。

エージェント名の入力と言語を日本語にしてSAVE。 f:id:nullpobug:20170606202324p:plain

エンティティを登録する

エンティティは、ユーザー入力から得られる値の定義です。

左メニューのENTITIESのところにある「+」ボタンをクリックしてエンティティの追加画面を表示。 f:id:nullpobug:20170606202331p:plain エンティティ名をfood、値を「お好み焼き」「たこ焼き」で入力してSAVE。 f:id:nullpobug:20170606202335p:plain ここで定義するもの以外にシステム定義のものや自由入力も扱えます。

インテントを登録する

インテントは、ユーザー発言をどのように変換、取り扱うかのルールの定義です。

左メニューのINTENTのところにある「+」ボタンをクリックしてインテントの追加画面を表示。 f:id:nullpobug:20170606205037p:plain UserSaysに「大阪でたこ焼き食べます」と入力してEnterキーを押すと、エンティティが認識されます。今回の場合、「大阪」の部分は @sys.geo-city 、「たこ焼き」の部分は作成しておいたエンティティの @food と判定されました。

f:id:nullpobug:20170606205038p:plain

@sys.geo-cityは組み込みのエンティティです。一覧はドキュメントに記載があります。

Entities · API.AI

SAVEボタンをクリックして保存します。

右ペインの「Try it now」のテキストボックスに、「東京でたこ焼きを食べます」と入力してパラメータの変換を試すと、geo-cityは「東京」、foodは「たこ焼き」として認識されました。 f:id:nullpobug:20170606205039p:plain

「SHOW JSON」ボタンをクリックすると、後述するfulfillmentサービスで外部に送信されるJSONを確認できます。 f:id:nullpobug:20170606205040p:plain

Webhook用のサーバーを用意する

今回はインテントで変換したパラメータを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サービスを設定する

fulfillmentは外部サービスなどでレスポンスを返す機能です。先程用意したWebhookでレスポンスを返すように設定します。

webhookはapi.ai側から通信できる必要があります。どこかにデプロイしてもいいのですが、お試しなのでngrokを使いました。

ngrokコマンドが使える状態で、次のコマンドを実行すると、localhost:5000にngrok経由で外部からアクセスできるようになります。

$ ngrok http 5000

fulfillmentのWebhookにURLを設定してSAVEしておきます。

f:id:nullpobug:20170606205035p:plain

また、インテントの編集画面の下部に「Use Webhook」というチェックボックスがあるので、チェックしてSAVEしておきます。

f:id:nullpobug:20170606205041p:plain

Slackのインテグレーションを設定する

Slackのbotとして動かすため、左メニューの「Integrations」からSlackのインテグレーションをONにします。

Slackへの接続ダイアログが表示されるので、任意のスラックグループを選びます。個人用のSlackを作って試すことをオススメします。

f:id:nullpobug:20170606205036p:plain

Slackのインテグレーションを有効にすると、Slack側にはapiai_botというアカウントが現れます。

動作確認

Slackでテスト用に追加されたapiai_botにDMで話しかけてみます。

正常動作していれば、botから応答があります。

f:id:nullpobug:20170606205043p:plain

与えた文章から得たパラメータで応答されることを確認できました。

感想

自然言語処理の部分を作り込むのは結構面倒なものですが、API.AIが肩代わりしてくれるのはうれしいですね。

API.AIのコンソールのUIがわかりづらい(どこがクリックできるのか、入力できるのかなど見た目でわからない、ラベルがない入力エリアなど)のですが、今後改善されることに期待したいです。

学習部分の調整は大変そうですが、いろいろ試していきたいです。

bpmappers 0.9

bpmappersのバージョン0.9を公開しました。

pypi.python.org

リポジトリ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

django-ftpserverのバージョンを0.5.0に上げました。

pypi.python.org

pyftpdlibのFileSystemにDjangoStorageAPIをつなげる機能を追加してみました。

StorageAPIとつなげると、面白そうだなーというのは前から考えてたんですが、StorageAPI側の機能が足りなくてラッパーが必須でいい解決方法が思い浮かばず放置になってました。

DjangoのStorageAPIはバージョンが上がるにつれて、ちょっとずつ機能が増えて、ようやく少しのラッピングでFileSystemの実装ができそうだったので、やってみた感じ。

django-storages経由でS3にFTPクライアントでつなげたり、django-gcloud-storage経由でGCSにつなげたりして遊んでました。

たのしい(^o^)

プログラミング学習サービスPyQの紹介

PyQというプログラミング学習サービスの紹介と宣伝です。

 ↓ これ ↓

以前、私が勤務していた株式会社ビープラウドが開発しています。

ビープラウドは、Python言語が得意でシステム開発をやってる会社です。connpassというイベントサイトの開発、運営もやってますね。

PyQとはどんなモノ?

Webブラウザ上で、プログラムコードを記述、実行しながら学習できるサービスです。

プログラミング言語は今のところPythonをターゲットにしているようです。

画面はこんな感じ。左側に説明文が表示されて、右側にエディタと実行結果の表示などが並びます。

f:id:nullpobug:20170518110921p:plain

見やすいです。

コンテンツ量(問題数)は結構ある

f:id:nullpobug:20170518115004p:plain

使ってみるとわかりますが、問題数は結構多いです。1つずつこなしていくとかなり時間がかかります。1日1時間ぐらいのペースで進めても、2~3ヶ月くらいかかりそうな感じ。

これからもコンテンツは増えていくようなので、期待できます。

環境構築の手間とハマりどころを避けられる

PyQを使う大きな理由の一つかと思います。

プログラミングを学習する場合、最初に学習用の環境構築(ソフトウェアのインストールや設定)が必要になることがほとんどです。

OSやプログラミングの知識が無いと、この環境構築で結構手間取ります。

f:id:nullpobug:20170518115000p:plain

「プログラミングの勉強をしたいのに、準備でつまづいてしまう」

というのは、よくあります。私もはじめのころはつまづいてました。プログラミング研修の講師をしていたときも、つまづいている方を多く見かけました。

PyQは、Webブラウザ上でエディタやコマンドシェルを使えるので、環境構築が不要ですぐにはじめられます。複数のパソコンを所有してても、それぞれに環境を用意する必要もありません。

私も使ってみてますが、実際に書いたコードを動かせるのは良いですね。

高すぎない、安すぎない

f:id:nullpobug:20170518115003p:plain

この記事を書いている時点(2017/05)の時点では、サービス利用料金はライトプランで 2,980円/月 となってます。

個人学習ならライトプランでまずは十分かと思います。

プログラミング学習の本を1冊買うと3000円ぐらいはしますし、同程度の金額でPyQを1ヶ月使えるならありだなーと思います。

支払いのFAQを見ると、中途半端な日数で解約しても日割りで返金されるとあるので、ちょっと試してみてイマイチならすぐ解約するのも大丈夫そうです。

料金支払に関するFAQ — PyQ 1.0 ドキュメント

チーム管理機能がよい

f:id:nullpobug:20170518114959p:plain

PyQには、複数の利用者をチームとしてまとめて管理する機能があります。個人向けではなく、教育現場や会社などの組織向けの機能ですね。

PyQで学習している人の進捗具合を確認できる機能があります。誰がどの問題までクリアしたか、わかります。

講師や管理者などの指導する立場の方にはうれしい機能です。

物足りない点

まだ発展途上のサービスなので、細かな部分で気になるところもありますが、私が少し物足りないなーと思った点は次の通り。

  • 問題のジャンルがPython言語一般とWeb開発に寄っている
    • Pythonを使う分野は他にもいろいろあるので幅を広が広がるとうれしいですね。
  • プランを解約状態だと、回答した内容を見れない
    • せっかく書いたコードだし、学び終わってから振り返るときに見れるとうれしいなーと。

上記は2017/05の段階で書いてますので、今後改善される可能性があります。気がついたら内容を更新します。

こういう人におすすめです

  • 個人でプログラミングの学習をしてみたい方(OSやパソコンの深い知識は無くても大丈夫)
    • (Python言語を勉強してみたい方には強くオススメします)
  • Python言語を使う業務のための準備として勉強したい社会人の方
  • 企業で新人向けの研修としてプログラミングをさせたい、という方
  • 学校で学生向けにプログラミングを勉強させたい、という方

f:id:nullpobug:20170518115001p:plain

私はどちらかというと教える側なので、チーム管理の機能が便利だし、講師をやるときには使っていきたいなーという感じです。

そんな感じでPyQを応援しているので、よろしくお願いします。