はじめに
今年のPyCon 2017(アメリカ)で、「API Star」という Web API FrameWork が発表され、そのトークを見て、密かにこのWAFに興味を持ったので、今回ちょっと触ってみました。
作ってみたものは、ただのAPI Client なので、完全に車輪の再発明ですが、APIStarがどんな感じか知りたかったので、まあよしなに。。
トーク動画はこちら↓
https://www.youtube.com/watch?v=Rk6MHZdust4&t=27s
目次
- この記事ですること
- セットアップ
- API Client の実装
- Viewへの表示
- まとめ
この記事ですること
- 何らかの検索キーワードをなげて、YouTube Data API の結果を取得
- 気持ち寂しいので、Vue.jsで取得結果も表示してみる
セットアップ
- 今回使用する
Python
のバージョンは、現時点で最新版の3.6.2
です。
セットアップの手順は、READMEの記載そのままにすすめる
pip3 install apistar
apistar new youtube_api_client
apistar/app.py
apistar/tests.py
こちらは、async/awaitに対応したセットアップも可能で、興味深いですが、今回は、割愛
API Client の実装
まずは、google-api-python-client
の追加と、APIキーを管理するための設定を追加
pip install google-api-client python-dotenv
touch .env settings.py
.envファイルには、発行されたAPIキーを。settings.pyは以下のように作成
API_KEY=xxxxxx
# settings.py
import os
from os.path import join, dirname
from dotenv import load_dotenv
dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)
API_KEY= os.environ.get("API_KEY")
次に、APIの結果を取得するクラスを作成
リクエスト可能なパラメータは、検索キーワード、検索順を指定の2つ
# !/usr/bin/python
from apiclient.discovery import build
from apiclient.errors import HttpError
import settings
# Set DEVELOPER_KEY to the API key value from the APIs & auth > Registered apps
# tab of
# https://cloud.google.com/console
# Please ensure that you have enabled the YouTube Data API for your project.
DEVELOPER_KEY = settings.API_KEY
YOUTUBE_API_SERVICE_NAME = "youtube"
YOUTUBE_API_VERSION = "v3"
class Client(object):
def search(self, query, order_by):
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=DEVELOPER_KEY)
try:
search_response = youtube.search().list(
q=query,
part='id,snippet',
type='video',
maxResults=25,
order=order_by
).execute()
return [search_result for search_result in search_response.get('items', [])]
except HttpError as e:
print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content))
raise e
そして、いよいよAPIStarの機能を使って、YouTube APIの取得結果から、このアプリケーションのAPIとしてURLを追加する。
import typing, json
from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls
from apistar import typesystem
from youtube_client import Client
class Query(typesystem.String):
min_length = 1
max_length = 250
class Order(typesystem.Enum):
enum = ['date', 'rating', 'relevance', 'title', 'videoCount', 'viewCount']
def welcome(name=None):
if name is None:
return {'message': 'Welcome to API Star!'}
return {'message': 'Welcome to API Star, %s!' % name}
def search_video(query: Query, order_by: Order):
if query is None:
return {'message': 'query is empty!'}
else:
videos = Client().search(query, order_by)
return videos
routes = [
Route('/', 'GET', welcome),
Route('/search', 'GET', search_video),
Include('/docs', docs_urls),
Include('/static', static_urls)
]
app = App(routes=routes)
if __name__ == '__main__':
app.main()
少し解説すると、まずclass Query
, class Order
で、Pythonの型ヒントを利用したapistar.typesystem
で、パラメータのバリデーションを行うことができる。
例えば、class Query
では、String型として、文字数の最小・最大数や、文字列が指定した正規表現にマッチするかなどがバリデーションとして設定できる。
それぞれの説明については、READMEのTypeSystem:API Referenceに記載されている。
ここでの、APIのイメージは、/search?query=検索キーワード&order_by=date
にリクエストしたら、キーワードに関連する動画の情報を取得するというものである。
それを実現するのが、app.py
の以下の部分。
def search_video(query: Query, order_by: Order):
if query is None:
return {'message': 'query is empty!'}
else:
videos = Client().search(query, order_by)
return videos
routes = [
Route('/', 'GET', welcome),
Route('/search', 'GET', search_video),
Include('/docs', docs_urls),
Include('/static', static_urls)
]
まず、routes
内でRoute(path, http_method, method)
というフォーマットで、ルーティングを定義できるのが、なんとなくわかる。
search_video
メソッドでは、引数にYouTube API のClientクラスを呼び出し、引数の型に、それぞれQuery
,Order
を定義し、検索結果を取得する
個人的には、パラメータのバリデーションの責務をQuery
,Order
が持つことになるので、search_video
メソッドでは、パラメータが正しいかどうかを気にする必要がなくなるので良さげ
(実際は、if query is None
という条件分岐があるので、完全にバリデーションを気にしていないといえば嘘になる。これは、僕のAPIStarの理解がまだ乏しいので、class Query
内で、検索キーワードが、None
であるかどうかをチェックするバリデーション方法がちょっとわからなかったためです)
実際に起動して、結果を確認してみる
apistar run
* Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 454-248-548
localhost:8080/search?query=ももクロ&order_by=date
確かに取得できている!!
なんということでしょう! メソッド定義して、routes
にぶち込むだけで、ここまでやってくれる!なんて素敵なのかしら
APIStarの便利なところは、/docs
があるように、実装として、APIを追加すると、自動的にドキュメントとしても追加してくれることである。
僕が、このフレームワークに興味を持った大きな理由はこの機能があるからと言っても過言ではない。
実際のユースケースとしては、作者のTom Christieが、動画のトーク中に説明していたが、サーバーサイドとフロントエンドで、開発チームが分かれている際に、APIStarで、迅速にAPIのモックを作成して、フロントエンド開発のスタートを早めることができることだと思う。
以下の画像のように、PythonとJavaScriptそれぞれのClientの実装が用意されていることからも、そのような背景が、考慮されていることがわかる。
Viewへの表示
APIStarは、Web API作成に特化したフレームワークだが、通常のWebアプリケーションと同様に、テンプレートを作成して、Viewの表示をすることもできる。
せっかくなので、フロントエンドのフレームワークは、特に理由なくVue.jsを使用します。
なぜなら、Vue.jsは最高だからです
HTMLで表示するページは、URLでいえば、/
にしようと思います。
そのため、html,jsの各種ファイルをを作成して、app.py
の内容を少し変更します。
touch templates/index.html
touch static/js/app.js
import typing, json
+ from apistar import Include, Route, annotate
+ from apistar.interfaces import Templates
+ from apistar.renderers import HTMLRenderer
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls
from apistar import typesystem
from youtube_client import Client
class Query(typesystem.String):
min_length = 1
max_length = 250
class Order(typesystem.Enum):
enum = ['date', 'rating', 'relevance', 'title', 'videoCount', 'viewCount']
- def welcome(name=None):
- if name is None:
- return {'message': 'Welcome to API Star!'}
- return {'message': 'Welcome to API Star, %s!' % name}
+ @annotate(renderers=[HTMLRenderer()])
+ def welcome(username: str, templates: Templates):
+ index = templates.get_template('index.html')
+ return index.render(username=username)
def search_video(query: Query, order_by: Order):
if query is None:
return {'message': 'query is empty!'}
else:
videos = Client().search(query, order_by)
return videos
routes = [
Route('/', 'GET', welcome),
Route('/search', 'GET', search_video),
Include('/docs', docs_urls),
Include('/static', static_urls)
]
+ settings = {
+ 'TEMPLATES': {
+ 'ROOT_DIR': 'templates',
+ 'PACKAGE_DIRS': ['apistar']
+ },
+ 'STATICS': {
+ 'ROOT_DIR': 'static', # Include the 'static/' directory.
+ 'PACKAGE_DIRS': ['apistar'] # Include the built-in apistar static files.
+ }
+ }
- app = App(routes=routes)
+ app = App(routes=routes, settings=settings)
if __name__ == '__main__':
app.main()
新たに、HTML表示するために必要なパッケージを導入して、レスポンスをHTML形式にしたいAPIに対して、関数デコレーターの@annotate
で、HTMLで返すことを決めるイメージでしょうか。
ちなみに、json形式の場合は、以下のような感じ
from apistar.renderers import JSONRenderer
...
@annotate(renderers=[JSONRenderer()])
def download_current_dataset():
# do something...
HTMLとjsをそれぞれ作成
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.3.1/css/foundation.min.css">
<title>YouTube Client</title>
</head>
<body>
<div class="container" id="app">
<section class="callout secondary">
<h3 class="text-center">[[ message ]]</h3>
<form>
<div class="row">
<div class="large-3 columns text-center">
<input v-model="query" placeholder="キーワードを入力">
<select name="order_by" v-model="order_by">
<option disabled value="">Please select one</option>
<option value="date">date</option>
<option value="rating">rating</option>
<option value="relevance">relevance</option>
<option value="title">title</option>
<option value="videoCount">videoCount</option>
<option value="viewCount">viewCount</option>
</select>
<a @click="search_video" class="button expanded">Search</a>
</div>
</div>
</form>
</sction>
<div class="columns large-3 medium-6" v-for="result in results">
<div class="card">
<div class="card-divider">
[[ result.snippet.title ]]
</div>
<img :src="result.snippet.thumbnails.high.url">
<div class="card-section">
<p>[[ result.snippet.description ]].</p>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.4/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>
いろいろ、セットアップするのが面倒だったので、今回は簡単に、CDN経由で、ダイレクトにVue.js
とAxios
を追加
const app = new Vue({
el: '#app',
delimiters: ["[[", "]]"],
data: {
message: 'YouTube Client',
query: '',
order_by: '',
results: []
},
methods: {
search_video: function () {
axios.get('/search', {
params: {
query: this.query,
order_by: this.order_by
}
})
.then(response => { this.results = response.data; })
.catch(error => { console.log(error);
});
}
}
});
APIStarのテンプレートは、Jinja2
なので、mustache記法が、二重波括弧({{ }}
)で、被ってしまうので、Vue.js側で、デリミタを変更する必要があります。
delimiters: ["[[", "]]"],
それ以外は、特筆すべき点はなく、検索キーワードと検索順をパラメータに投げるメソッドを追加しただけ
画面はこんな感じ
デザインのやる気のなさには、ご容赦ください
ここで表示しているのは、title, description, サムネ画像だけですが、ちゃんと表示された。

まとめ
- APIのデータ構造化を、Python3の型ヒントでうまく利用しているイメージ
- さくっとモックを作れるし、しっかりAPIとして作りたいときも、フレームワークとして、JSONSchemaにも対応している
- 認証機能があったり、サーバレス環境へのデプロイなどフレームワークとしての守備範囲は広そう
- まだまだ開発途中なので、ドキュメントはREADMEとissueが頼り。フレームワーク自身の実装から、挙動を理解・確認する必要が多々あった。
- Railsと違って、ありがたい先人たちの日本語情報はほぼ皆無。英語情報のみ(個人的には、メリット
)
(読み返してみると、そんなにAPIStarの機能使った共有になってないなぁ...)