API Star を使って、YouTube Data API から動画情報を取得して表示する

# はじめに
今年の[PyCon 2017(アメリカ)](https://us.pycon.org/2017/about/)で、[「API Star」](https://github.com/encode/apistar#data-validation)という Web API FrameWork が発表され、そのトークを見て、密かにこのWAFに興味を持ったので、今回ちょっと触ってみました。

作ってみたものは、ただのAPI　Client なので、完全に車輪の再発明ですが、APIStarがどんな感じか知りたかったので、まあよしなに。。

トーク動画はこちら↓
https://www.youtube.com/watch?v=Rk6MHZdust4&t=27s

## 目次
1. この記事ですること
2. セットアップ
3. API Client の実装
4. Viewへの表示
5. まとめ

## この記事ですること
- 何らかの検索キーワードをなげて、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](https://github.com/encode/apistar#data-validation)に対応したセットアップも可能で、興味深いですが、今回は、割愛

## API Client の実装
まずは、`google-api-python-client`の追加と、APIキーを管理するための設定を追加

```
pip install google-api-client　python-dotenv
touch .env settings.py
```

.envファイルには、発行されたAPIキーを。settings.pyは以下のように作成

```.env
API_KEY=xxxxxx
```

```settings.py
# 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の結果を取得するクラスを作成
リクエスト可能なパラメータは、検索キーワード、検索順を指定の２つ

```youtube_api_client/youtube_client.py
#!/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を追加する。

```app.py
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](https://github.com/encode/apistar#api-reference)に記載されている。

ここでの、APIのイメージは、`/search?query=検索キーワード&order_by=date` にリクエストしたら、キーワードに関連する動画の情報を取得するというものである。
それを実現するのが、`app.py`の以下の部分。

```py3
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/docs`
<img width="1512" alt="スクリーンショット 2017-10-01 0.18.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/41385/4419eafe-fc34-d778-4814-75e27f5277ff.png">

`localhost:8080/search?query=ももクロ&order_by=date`
<img width="1014" alt="スクリーンショット 2017-10-01 0.20.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/41385/4a99ada4-aa7f-0d25-5d56-303814d9b0f4.png">

確かに取得できている！！
なんということでしょう！　メソッド定義して、`routes`にぶち込むだけで、ここまでやってくれる！なんて素敵なのかしら

APIStarの便利なところは、`/docs`があるように、実装として、APIを追加すると、自動的にドキュメントとしても追加してくれることである。

僕が、このフレームワークに興味を持った大きな理由はこの機能があるからと言っても過言ではない。

実際のユースケースとしては、作者の[Tom Christie](https://github.com/tomchristie)が、動画のトーク中に説明していたが、サーバーサイドとフロントエンドで、開発チームが分かれている際に、APIStarで、迅速にAPIのモックを作成して、フロントエンド開発のスタートを早めることができることだと思う。

以下の画像のように、PythonとJavaScriptそれぞれのClientの実装が用意されていることからも、そのような背景が、考慮されていることがわかる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/41385/d3aa7c62-b952-9188-2df7-5f854237a7d8.png)

## Viewへの表示
APIStarは、Web API作成に特化したフレームワークだが、通常のWebアプリケーションと同様に、テンプレートを作成して、Viewの表示をすることもできる。

せっかくなので、フロントエンドのフレームワークは、特に理由なくVue.jsを使用します。
なぜなら、Vue.jsは最高だからです:sunglasses:

HTMLで表示するページは、URLでいえば、`/`にしようと思います。
そのため、html,jsの各種ファイルをを作成して、`app.py`の内容を少し変更します。

```
touch templates/index.html
touch static/js/app.js
```

```diff:app.py
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形式の場合は、以下のような感じ

```py3
from apistar.renderers import JSONRenderer

...
@annotate(renderers=[JSONRenderer()])
def download_current_dataset():
    # do something...

```

HTMLとjsをそれぞれ作成

```templates/index.html
<!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`を追加

```static/js/app.js
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: ["[[", "]]"],
```

それ以外は、特筆すべき点はなく、検索キーワードと検索順をパラメータに投げるメソッドを追加しただけ

### 画面はこんな感じ
デザインのやる気のなさには、ご容赦ください:bow:

ここで表示しているのは、title, description, サムネ画像だけですが、ちゃんと表示された。

<img width="1139" alt="スクリーンショット 2017-10-01 17.24.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/41385/f731f5d5-6170-99a2-6c43-9895b0f4851b.png">

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

~~(読み返してみると、そんなにAPIStarの機能使った共有になってないなぁ...:sweat_smile:)~~

## 参考
- https://github.com/encode/apistar
- https://discuss.apistar.org/
- [API star - Python3用 Web API framework](http://atsuoishimoto.hatenablog.com/)
- [Tom Christie Level up! Rethinking the Web API framework PyCon 2017
](https://www.youtube.com/watch?v=Rk6MHZdust4)
- [Treehouse LiveCoding: APIStar](https://www.youtube.com/watch?v=j7Na7lBP0UI)
- [Treehouse LiveCoding: API Star (week 2)
](https://www.youtube.com/watch?v=VhzQqMSlNKo)