pytestを使ったFlaskのテスト
はじめに
普段のデータ分析や機械学習モデリングをやっているのでテストコードを書くことは多くなかったので、Flaskでアプリケーションを作り出すとテストをどう具体的に書いていいのかわからなかった。基本的なことはTesting Flask Applicationsを参考にしたものの、Pytestを使っていて気になったテストの書き方をまとめる。基本的なテストの書き方やpytestの使い方については、テスト駆動Python(翔泳社、2018)で学んだ。
- 作者: Brian Okken,株式会社クイープ,安井力
- 出版社/メーカー: 翔泳社
- 発売日: 2018/08/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
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が後継だと強く言っているようなので、今回はこちらを選択した。
開発環境の設定
- GCPのコンソール上でFirebaseをenableにする
- gcloudコマンドのインストール
- サービスアカウント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.txt
。google-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
が返ってくる。
知っているidを入力すると、ヒーローの名前を知ることができる。
まとめ
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を入れてる必要があったのが辛かった。
CircleCIの設定
CircleCIからgcloud SDKを使ってデプロイする際にはサービスアカウントを認証する必要があります。サービスアカウントの認証方法は、下記3つありますが、CircleCI上からはブラウザを使った認証が出来ないので、3を使います。
- gcloud init を使う
- gcloud auth login を使う
- 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)パターン勉強会に参加してきました。実例や既に公開されているパターンをベースに機械学習システムにおける、デザインパターン、アンチパターン、プロセスパターン、組織のパターンについて議論することで機械学習システムの開発のプロセスやアーキテクチャにパターンを見つけ、名前をつけ、熟練者のノウハウや思考法を横展開できるようにすることが会の目的となっていました。
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版 知っておきたいプログラムの基礎知識
- 作者: 矢沢久雄
- 出版社/メーカー: 日経ソフトウエア
- 発売日: 2007/04
- メディア: 単行本(ソフトカバー)
- 購入: 45人 クリック: 646回
- この商品を含むブログ (74件) を見る
内容は、
- CPUの仕組み
- データの2進数での取り扱い方
- 浮動小数点の扱い
- メモリーの仕組み
- コンパイルとアセンブリ
- ハードウェアの制御
のようになっており、コンピューターをなんかよくわからんが計算してくれる箱だと思っている自分としてはこれがきっと基礎なんだろうなという内容が並んでいる。
解説のために出てくるCのコードをGoに置き換えながら読もうかと思いましたが、意外とC言語のコード少なく特別なこともしてなかったので、自身としてはあんまり効果なかったかなって感じです。一応リポジトリにコード置いて少しづつ進めます。
良かった点
特にメモリの仕組みや、コンパイル、アセンブリ言語のあたりはpythonでのデータ分析や機械学習モデリングをメインで取り扱っていると隠遁されていて意識していなかった部分だったので多少イメージがつくようになりました。 特に10章のC言語とアセンブリ言語を1対1で対応付けて解釈していく部分は、プログラミング言語の仕組みが腹に落ちた気がして、LLじゃなくても怖くないやん!となれたのはでかい。システムプログラミングをやろうと年末年始にC言語の勉強をしていたのはまじでためになっている気がする。CとGoそれぞれから作られたアセンブリの比較とかしたいな。時間あるときにやる。
会社のエンジニアも言ってたけど、この類の低レベルの話はすぐに何かが出来るようになるわけでもなく、誰かに褒められるわけでもないが、とはいえ知らないまんまだと新しい技術へのキャッチアップが進まんくなるというのはすごく納得感あるので継続して勉強していきたい。
次はGoならわかるシステムプログラミングやります。
- 作者: 渋川よしき,ごっちん
- 出版社/メーカー: ラムダノート
- 発売日: 2017/10/23
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る