pytestを使ったFlaskのテスト

はじめに

普段のデータ分析や機械学習モデリングをやっているのでテストコードを書くことは多くなかったので、Flaskでアプリケーションを作り出すとテストをどう具体的に書いていいのかわからなかった。基本的なことはTesting Flask Applicationsを参考にしたものの、Pytestを使っていて気になったテストの書き方をまとめる。基本的なテストの書き方やpytestの使い方については、テスト駆動Python(翔泳社、2018)で学んだ。

テスト駆動Python

テスト駆動Python

pytestを使っていて気になった点

カバレッジの出し方

pytest-covを使う。カバーできていない部分を把握するためにterm-missingを使うのが良い。

pytest -v --cov=source-dir --cov-report=term-missing

出力結果は下のようになる。Missingの絡むがカバーしきれていない行数として表される。

-------- coverage: platform linux, python 3.6.7-final-0 -----------
Name          Stmts   Miss  Cover   Missing
-------------------------------------------
__init__.py       0      0   100%
conftest.py       9      0   100%
test_app.py      29     10    66%   70-85
-------------------------------------------
TOTAL            38     10    7

flake8によるコードチェックの実行

ソースコードやテストコードの中にflake8非準拠なコードがあると検出してくれるpytest-flakes。念の為やっておく感じ。

pytest --flakes                                                                                                      [dev]

もし準拠していないコードがあると指摘してくる。

========================================================== test session starts ==========================================================
platform linux -- Python 3.6.7, pytest-4.4.0, py-1.8.0, pluggy-0.9.0
rootdir: /root/work/podcast2text
plugins: flakes-4.0.0, cov-2.6.1
collected 9 items

__init__.py s                                                                                                                     [ 11%]
conftest.py s                                                                                                                     [ 22%]
test_app.py F....s.                                                                                                               [100%]

=============================================================== FAILURES ================================================================
____________________________________________________________ pyflakes-check _____________________________________________________________
/root/work/podcast2text/tests/test_app.py:73: UndefinedName
undefined name 'storage'
============================================= 1 failed, 5 passed, 3 skipped in 0.99 seconds =============================================

POSTメソッドでデータを送る場合

test_clientにPOSTメソッドでdataをもたせればよかっただけだった。

from io import BytesIO


def test_post_upload_with_file(test_client):
    """
    GIVEN a Flask application
    WHEN the '/upload' page is requested (POST) with files
    THEN check the response is valid
    """
    response = test_client.post(
        '/upload',
        data={
            'file': (BytesIO(b'my file contents'), 'file.txt')
        }
    )

    assert response.status_code == 200

参考

GAEへpythonとFirestoreを使ったサービスをデプロイする

はじめに

Python3.7のruntimeがGAE standard environmentで使えるようになったため、flaskでアプリを作ってGAEへデプロイしている。DBにDatastoreとFirestoreが選択肢に入ってくるが、GoogleがFirestoreが後継だと強く言っているようなので、今回はこちらを選択した。

開発環境の設定

  1. GCPのコンソール上でFirebaseをenableにする
  2. gcloudコマンドのインストール
  3. サービスアカウントJSON fileをダウンロードし、GOOGLE_APPLICATION_CREDENTIALSへ割り当てる

FirebaseのサービスアカウントのJSONファイルはここから手に入れることができる。

$ curl https://sdk.cloud.google.com | bash
$ exec -l $SHELL
$ gcloud init
$ export GOOGLE_APPLICATION_CREDENTIALS=path/to/creds.json

サービスの実装

準備するファイルは下記。 - requirements.txt: dependenciesを宣言 - main.py: アプリのソースコード - app.yaml: GAEへのデプロイ用

まずはrequirements.txtgoogle-cloud-firestoreをあえて入れる理由はpipのバグがあるかららしい。本来はfirebase-adminだけで十分とのこと。

Flask==1.0.2
firebase-admin==2.13.0
google-cloud-firestore==0.29.0

次にmain.py。Firebase Admin SDKでクライアントを作っている。

import firebase_admin
import flask
from firebase_admin import firestore
from flask import request

app = flask.Flask(__name__)

firebase_admin.initialize_app()
SUPERHEROES = firestore.client().collection('superheroes')


@app.route('/', methods=['GET'])
def index():
    return """
    <h1> Create hero? </h1>
    <form method="post" action='/heroes'>
        <input type="text" size="30" name="hero name">
        <input type="submit" value="create hero">
    </form>
    <h1> Read hero? </h1> 
    <form method="post" action='/read'>
        <input type="text" size="30"name="id">
        <input type="submit" value="read hero">
    </form> 
    """


@app.route('/heroes', methods=['POST'])
def create_hero():
    req = {"name": request.form['hero name']}
    hero = SUPERHEROES.document()
    hero.set(req)
    return """
    <h1> id': %s </h1>
    <form method="get" action='/'>
        <input type="submit" size="30" value="back to the index">
    </form>
    """ % hero.id


@app.route('/read', methods=['POST'])
def read_hero():
    id = request.form['id']
    heroname = _ensure_hero(id).to_dict()
    return """
    <h1> hero is %s </h1>
    <form method="get" action='/'>
        <input type="submit" value="back to the index">
    </form>
    """ % heroname['name']


def _ensure_hero(id):
    try:
        return SUPERHEROES.document(id).get()
    except:
        flask.abort(404)


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)

最後にapp.yaml。runtimeにpython3.7を指定。

runtime: python37
service: heroe

GAEへのデプロイと動作の確認

GAEへのデプロイはgcloud app deployで行う。

Beginning deployment of service [heroes]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 1 files to Google Cloud Storage                      ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [heroes]...done.
Setting traffic split for service [heroes]...done.
Deployed service [heroes] to [https://heroes-dot-my-project.appspot.com]

ヒーローの名前を入力すると、Firestore上で割り当てられたidが返ってくる。

f:id:tkzs:20190406190242p:plain

f:id:tkzs:20190406190608p:plain

知っているidを入力すると、ヒーローの名前を知ることができる。

f:id:tkzs:20190406190900p:plain

まとめ

GAEからFirestoreを使うには、firestoreのクライアントを呼び出して使うだけで簡単。

参考

Firebase: Developing an App Engine service with Python and Cloud Firestore

GCPのサービスアカウントについて

はじめに

GCP使ってアプリ開発をしていて、サービスアカウントのJSONファイルを環境変数に設定することがあるが、割と雰囲気で使っているのできちんと理解したくなった。さらに元をたどって、Cloud IAMも合わせて理解しようと思う。

Cloud IAMとは

Cloud Identity and Access Management (Cloud IAM) とは、下記のようなIAMポリシーを定めて管理するもの。

「誰が(メンバー)」 - Google アカウント:個人のメールアドレスに紐付いたアカウント。 - サービスアカウント:アプリケーションに付随するアカウント。 - Google グループ:Googleアカウントやサービスアカウントのグループ。まとめてポリシーを変更できるので管理が楽になる。 - GSuite ドメイン:組織全体は同じドメインであるなら、その中を一つの仮想グループにして管理しよう、という話。 - Cloud Identity :GSuiteと似ているが、GSuiteの機能にはアクセスできない。

「何に(リソース)」 - GCPプロジェクト - Compute Engine インスタンス - Cloud Storage バケット など

「何ができるか」 - Read only/wirteなど

サービスアカウント

GoogleAPIを使うユーザーが、人に紐づくIDではなくて、サービスアカウントのIDであることを想定している場合に使うgoogleアカウント。 管理の方法は色々あるが、ひとまずIAM&admin->Service accountsで行っている。サービスアカウントを作成するとKEYが発行でき、JSON形式でダウンロードが可能。これをGOOGLE_APPLICATION_CREDENTIALSへ割り当てることであるサービスがGCPの他のサービスを使う際の権限を管理することになる。

参考

flaskでGAEからGSCにファイルをアップロードするまで

GAE standard environmentからGCSへファイルをアップロードしたい。 下記にあるようにGAE standard eivironmentではclinet libraryが異なるので注意が必要。

pip install --upgrade google-cloud-storage

認証を通すためにsecret.jsonへ環境変数を設定しておく。

export GOOGLE_APPLICATION_CREDENTIALS="/path/to/file/[FILE_NAME].json"
from flask import request
from google.cloud import storage

@app.route('/upload', method=['POST'])
def upload():

  if 'file' not in request.files:
    return 'No file uploaded.', 400
    
  uploaded_file = request.files.get('file')
  client = storage.Client()
  bucket = client.get_bucket('bucket-id-here')
  blob = bucket.blob('blob-name')
  blob.upload_from_file(uploaded_file)
 ```

## 参考

[https://pypi.org/project/google-cloud-storage/:title]
[https://googleapis.github.io/google-cloud-python/latest/storage/blobs.html#google.cloud.storage.blob.Blob.upload_from_file:title]

CircleCI2.0からGAEスタンダード環境のruntime:python3.7へデプロイする手順

概要

GAEのスタンダード環境のpython3.7を使う際に、CircleCIから直接デプロイできるようにした。CircleCIがDockerコンテナベースでのビルドなので、そこからgcloud SDKでデプロイするための設定をすればOK。基本的にpython3でAppを作ってるけど、gcloud SDKがpython2で動くので共存させる必要がある。

今回はpython2、python3、Google Cloud SDKを入れてあるDockerイメージを用意し、CircleCIの.circleci/config.ymlの中で呼び出し、その中でgcloudコマンドを叩いてデプロイするようにしました。gcloudでデプロイするためにcircleciで立ち上げたコンテナの中でサービスアカウントを認証しなければならない点です。

Docker image

Python3.7-strechのイメージに、python2.7、Google Cloud SDKが入ったDockerイメージを自作しました。下記のDockerhubのリポジトリに入っていますので、必要に応じてCircleCIで使うなりpulsるなりlして使って下さい。 Dockerfileの入ったリポジトリは下記です。最初pip3がうまく起動しなかったので、gcloudSDK, python2を入れた後に再度pip3を入れてる必要があったのが辛かった。

github.com

CircleCIの設定

CircleCIからgcloud SDKを使ってデプロイする際にはサービスアカウントを認証する必要があります。サービスアカウントの認証方法は、下記3つありますが、CircleCI上からはブラウザを使った認証が出来ないので、3を使います。

  1. gcloud init を使う
  2. gcloud auth login を使う
  3. cloud auth activate-service-account --key-file を使う

この3つ目の認証には、

  • サービスアカウントのメールアドレス
  • サービスアカウントの認証キー
  • サービスID

3つを用意しなければいけません。 サービスアカウントはGCPのコンソールからIAMタブへいき、アドレスと認証キーを入手し、手元に控えて下さい。 このアドレスと認証キーをconfig.yamlに直接書いたりするのは管理面で怖いです。よってCircleCIのアプリの中でEnvironment Variablesというconfig.ymlに環境変数を渡す機能があるので、それを使って渡すことになります。CircleCIアプリの中で個別のプロジェクトのPROJECT SETTINGSで設定しましょう。認証キーはJSONの中身をそのままコピペするだけで大丈夫です。

設定が完了したら.circleci/config.yamlを書きます。関連する部分だけを書き出すと下記のようになります。

version: 2
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: tkazusa/appengine-python37:latest
    steps:
      - checkout

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt
        
      - run:
          name: google auth
          command: |
            echo $DEV_SERVICE_ACCOUNT_KEY > /tmp/secret.json
            gcloud auth activate-service-account $DEV_SERVICE_ACCOUNT_CLIENT_EMAIL --key-file /tmp/secret.json
      - run:
          name: deploy production
          command: |
            gcloud --quiet config set project $GOOGLE_PROJECT_ID
            gcloud --quiet app deploy app/app.yaml --project $GOOGLE_PROJECT_ID

まとめ

Dockerコンテナの準備がややめんどくさかったですけど、これで個人でもCircleCIからGAEにCDできるようになりました。DockerhubのAutobuildも勉強できて良かった。

参考

機械学習応用システム(MLS)パターン勉強会に参加してきた

概要

機械学習工学研究会(MLSE)の機械学習応用システム(MLS)パターン勉強会に参加してきました。実例や既に公開されているパターンをベースに機械学習システムにおける、デザインパターン、アンチパターン、プロセスパターン、組織のパターンについて議論することで機械学習システムの開発のプロセスやアーキテクチャにパターンを見つけ、名前をつけ、熟練者のノウハウや思考法を横展開できるようにすることが会の目的となっていました。

mlxse.connpass.com

19:00-19:20 パターンとは?:鷲崎教授(早稲田大学)
19:20-19:30 チーム編成
19:30-20:30 チームごとにパターンの議論
20:30-21:00 発表
21:00-21:30 議論&懇親

実際にやったこと

パターンについてのガイダンス

勉強会冒頭でパターンの権威である鷲崎教授(早稲田大学)にガイダンスをいただきました。もともと建築の世界で使われていたパタン及びパタンランゲージにオブジェクト指向なソフトウェア業界が取り入れていったというパタンの歴史的な経緯や、その見つけ方などをさらっと20分。ソフトウェア開発におけるデザインパターンなんかは、経験的に良い繰り返しパターンを実践に持ち込んだ形だし、アジャイル開発が「繰り返しを見つけていこう」というマイニングの手続き活動が流行らなかったことを反映して、「こういうやり方でいきましょう」と割り切って決めたひとつのプロセスパタンの形という話は面白かった。

パタンについてもう少し

熟練者のノウハウを横展するためにパタンを探し出して名前をつける。そのMinimalとしては「こういうとき(文脈)に、こういうことで困る(課題)ので、こうしよう(解決策)」で成り立つが、展開する際に意図や適用法が正しく伝わらないようなことが起こり得るので、もう少し詳細に下記のような項目で検討するらしい。 - 状況(文脈) - 問題 - フォース(考慮するべき点) - 解決策 - 結果

フォースの概念が難しいが、文脈における制約事項や懸念事項だと考えれば良いっぽい。

チームごとにパターンの議論

数名ずつ下記の4つのテーマについて議論するチームに分かれてパタンを洗い出しまとめる作業を実施しました。

  • データ取得、加工、管理
  • 組織、マネジメント、プロセス
  • 要求、設計、CHI
  • テスト、検証、運用

色々アウトプットが出てきたのですが、その中で面白いなと思ったパタンが下記です。PFNさんとDeNAさんで同じようなパタンで業務を行っていたとのことで妥当性があるんだろうなと。自分の業務の中で考えても納得感がある。

  • MLアルゴリズム開発とインフラ分離のパターン
    • 文脈:機械学習には複雑なインフラが必要になりがち
    • 課題:MLエンジニアやリサーチャがアルゴリズム開発に集中できない
      • インフラやアノテーションツールを開発して、時間が足りなくなる
    • フォース:インフラ、アノテ、アルゴリズムは専門性が異なりかつ疎結合にできる
    • 解決法:チームをある程度分離してしう
    • 結果:アルゴリズム開発者が主業務に集中できる

感想

そろそろAIブームも落ち着いてきて、普通の技術として普及期に入ってきているので共通言語を持って話せば、もっともっと社会実装が進むだろうなと感じた。各所で勉強会たくさんやってるけど、その内容まとめて整理・編集してパタンに名前をつけるような作業はどこでやればいいんだろう。Web業界との親和性高いからか、いろんなところで発表された内容が消費されてるだけ感あるしな、ここんところ。

【書評】プログラムはなぜ動くのか 第2版

中身の概要

データ分析のためにプログラミングを使う立場で計算機と付き合ってきたが、機会学習モデルをシステムとして本番環境にデプロイするようになってくると、コンピューターのこと知らなさ過ぎることの辛みが出てきたので、今年は計算機や情報工学についてもう少し詳しくなる目的で勉強を始めました。その第一弾がプログラムはなぜ動くのか 第2版です。

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識

内容は、

  • CPUの仕組み
  • データの2進数での取り扱い方
  • 浮動小数点の扱い
  • メモリーの仕組み
  • コンパイルとアセンブリ
  • ハードウェアの制御

のようになっており、コンピューターをなんかよくわからんが計算してくれる箱だと思っている自分としてはこれがきっと基礎なんだろうなという内容が並んでいる。

解説のために出てくるCのコードをGoに置き換えながら読もうかと思いましたが、意外とC言語のコード少なく特別なこともしてなかったので、自身としてはあんまり効果なかったかなって感じです。一応リポジトリにコード置いて少しづつ進めます。

良かった点

特にメモリの仕組みや、コンパイル、アセンブリ言語のあたりはpythonでのデータ分析や機械学習モデリングをメインで取り扱っていると隠遁されていて意識していなかった部分だったので多少イメージがつくようになりました。 特に10章のC言語とアセンブリ言語を1対1で対応付けて解釈していく部分は、プログラミング言語の仕組みが腹に落ちた気がして、LLじゃなくても怖くないやん!となれたのはでかい。システムプログラミングをやろうと年末年始にC言語の勉強をしていたのはまじでためになっている気がする。CとGoそれぞれから作られたアセンブリの比較とかしたいな。時間あるときにやる。

会社のエンジニアも言ってたけど、この類の低レベルの話はすぐに何かが出来るようになるわけでもなく、誰かに褒められるわけでもないが、とはいえ知らないまんまだと新しい技術へのキャッチアップが進まんくなるというのはすごく納得感あるので継続して勉強していきたい。

次はGoならわかるシステムプログラミングやります。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング