[翻訳] httr vignette: APIパッケージ作成のベストプラクティス

  • 8
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この文書は Hadley Wickham によるRパッケージhttr (version 1.1.0) のビネット "Best practices for writing an API package" の翻訳です.
License: MIT


APIパッケージ作成のベストプラクティス

この文書にはweb APIに接続するパッケージを書くためのベストプラクティスを集めています.目標は,安全かつセキュアで長期に渡って動作し続けるパッケージを読者が作成するのを手助けすることです.

もしweb APIに取り組むのが初めてであれば,zapierによる "An introduction to APIs" を読むことから始めてください.

重要な情報

APIを書くときには,そのAPIの一般的な規約を表現するヘルパ関数を書くことから始めるのがベストです.これらの関数は以下のような情報を表すことになります:

  • すべてのAPIリクエストのベースURL
  • 認証の動作方法
  • APIのバージョン
  • 結果(エラーも含む)の返却方法

以下ではgithub APIに対して,これらの関数の書き方の一例を示します.コードのくり返しをできるだけ避けるために,多数の小さな関数を使っていることに注目してください.これはAPIと通信するコードを書くときには重要なことです.というのも,APIはあまりに頻繁に変更されるので,重要な事項は一か所にまとめておいて,そこだけ変更すれば済むようにしたいからです.

GETPOSTのリクエストを実行する関数を書くことから始めましょう:

github_GET <- function(path, ..., pat = github_pat()) {
  auth <- github_auth(pat)
  req <- GET("https://api.github.com", path = path, auth, ...)
  github_check(req)

  req
}

github_POST <- function(path, body, ..., pat = github_pat()) {
  auth <- github_auth(pat)

  stopifnot(is.list(body))
  body_json <- jsonlite::toJSON(body)

  req <- POST("https://api.github.com", path = path, body = body_json,
    auth, post, ...)
  github_check(req)

  req
}

このコードには認証やレスポンスの確認,また有用なエラーメッセージの表示やレスポンスのパースを行うための追加のインフラが必要です.

github_auth <- function(pat = github_pat()) {
  authenticate(pat, "x-oauth-basic", "basic")
}

github_check <- function(req) {
  if (req$status_code < 400) return(invisible())

  message <- github_parse(req)$message
  stop("HTTP failure: ", req$status_code, "\n", message, call. = FALSE)
}

github_parse <- function(req) {
  text <- content(req, as = "text")
  if (identical(text, "")) stop("No output to parse", call. = FALSE)
  jsonlite::fromJSON(text, simplifyVector = FALSE)
}

github_pat <- function() {
  Sys.getenv('GITHUB_PAT')
}

has_pat <- function() !identical(github_pat(), "")

github_pat()は環境からパーソナルアクセストークンを取ってくるために挿入しているだけです.後ほど「認証」のセクションで,長くはなりますがもっと良い書き方を見ることになります.

いったんこれらのパーツを整えてしまえば,API関数を実装するのは簡単です.たとえば,github API の呼び出し可能回数がどれだけ残っているか知らせるrate_limit()関数を実装することができるでしょう.

rate_limit <- function() {
  req <- github_GET("rate_limit")
  github_parse(req)
}

if (has_pat()) {
  str(rate_limit())
}

最初のバージョンを動くようにした後には,大抵,出力がよりユーザフレンドリーになるように改良
したくなるでしょう.そのような例として,UNIXタイムスタンプをパースしてもっと有用なデータ型に変換することができます.

rate_limit <- function() {
  req <- github_GET("rate_limit")
  core <- github_parse(req)$resources$core

  reset <- as.POSIXct(core$reset, origin = "1970-01-01")
  cat(core$remaining, " / ", core$limit,
    " (Reset ", strftime(reset, "%H:%M:%S"), ")\n", sep = "")
}

if (has_pat()) {
  rate_limit()
}

APIの複雑さによっては,リクエストオブジェクトを返す関数と,それをパースして有用なRオブジェクトに変換する関数を分けておきたい場合もあるかもしれません.

出力のパースと入力のポスト

ほとんどのAPIはJSONかXMLで通信を行います.JSONを扱うにはjsonliteパッケージがおすすめです.XMLを扱うにはxmlパッケージを使ってください.

httr はcontent(..., as = "auto")でデフォルトのパーサを提供していますが,これをパッケージ内部で使用することは推奨しません.代わりにコンテンツはcontent(..., as = "text")で取得して,パースは自分で行います.APIは(めったに起こるべきではないのですが)無効なデータを返してくるかもしれないので,有用なエラーメッセージを提供してくれるパーサが頼りになります.

github_parse <- function(req) {
  text <- content(req, as = "text")
  if (identical(text, "")) stop("")
  jsonlite::fromJSON(text, simplifyVector = FALSE)
}

多くのAPIは送り返すデータの種類を決めるのにコンテントネゴシエーションを使用しています.もし読者がラッパーを書いているAPIがこの機能を使用している場合,accept_json()accept_xml()が有用だと思うかもしれません.

エラーに対処する

まずはHTTPステータスコードを確認しましょう.400番台のステータスコードは普通,あなたが何か誤ったことを意味しています.500番台のステータスコードは,概してサーバ側で何かが上手くいかなかったことを意味していますが,サーバに何かを間違った形式で送ってしまったのかもしれません.

エラーを受け取ったときには,大抵リクエストのボディに有用な情報が含まれているので,それをパースしてエラーの内容を抜き出すべきです.APIによって内容は異なりますが,github の場合は以下のようにしてパースすることができます:

github_parse <- function(req) {
  text <- content(req, as = "text")
  if (identical(text, "")) stop("No output to parse", call. = FALSE)
  jsonlite::fromJSON(text, simplifyVector = FALSE)
}

APIがよくある問題に対して特別なエラーを返す場合には,エラーについてより詳しい情報をユーザに提供したくなるかもしれません.たとえば,リクエストを使い果たして回数制限に引っかかった場合には,ユーザに次のリクエストが可能になるまでの待ち時間を知らせたいでしょう(あるいはそれまでの長い間,自動的に待機するようにしたくなるかも!).

認証

最も一般的な認証方式は OAuth とBasic認証です:

  • OAuth 1.0. もはやそれほど一般的な認証方式ではありません.詳細についてはoauth1.0_token()を見てください.
  • OAuth 2.0. モダンなwebアプリケーションでは非常に一般的な認証方式です.APIクライアントがデータにアクセスする権限を確立するために,クライアントとサーバの間でのラウンドトリップを行います.詳細については oauth2.0_token() を見てください.ユーザのデータのセキュリティにとって,APIキーと「シークレット」は実は重要ではないので,公開してもかまいません.
  • ユーザ名とパスワードによるBasic認証.最も多くの情報を要求するので,いちばんセキュアでない認証方式です.

    authenticate("username", "password")
    #> <request>
    #> Options:
    #> * httpauth: 1
    #> * userpwd: username:password
    
  • APIキーを用いたBasic認証.ますます多くのAPIによって提供されている代替的な認証方式です.ユーザアカウントと密接に関連しているユーザ名とパスワードを使う代わりに,ランダムに生成されたAPIキーを使用します.APIキーはユーザアカウントとは独立に無効化することができます.多くの場合,パスワードは空欄にするか,あらかじめ指定された値を使用します.

    authenticate("ddfa3d40d5855d6ba76b7003fd4", "")
    #> <request>
    #> Options:
    #> * httpauth: 1
    #> * userpwd: ddfa3d40d5855d6ba76b7003fd4:
    

ユーザクレデンシャルを何回も入力せずに済むように保持しておく方法も必要でしょう.OAuthを使用している場合は,httrが面倒を見てくれます.その他のユースケースの場合は,環境変数を利用することを推奨します.下記の関数はパーソナルアクセストークンを GITHUB_PATという環境変数から取り出します.環境変数が存在しない場合はセットの仕方を教えます.devtoolsパッケージではパッケージをプライベートリポジトリからインストールするためにパーソナルアクセストークンにアクセスする必要があります.

github_pat <- function(force = FALSE) {
  env <- Sys.getenv('GITHUB_PAT')
  if (!identical(env, "") && !force) return(env)

  if (!interactive()) {
    stop("Please set env var GITHUB_PAT to your github personal access token",
      call. = FALSE)
  }

  message("Couldn't find env var GITHUB_PAT. See ?github_pat for more details.")
  message("Please enter your PAT and press enter:")
  pat <- readline(": ")

  if (identical(pat, "")) {
    stop("Github personal access token entry failed", call. = FALSE)
  }

  message("Updating GITHUB_PAT env var to PAT")
  Sys.setenv(GITHUB_PAT = pat)

  pat
}

読者のパッケージのユーザには,重要な情報はコンソールに打ち込むよりも,一度だけ保存して済ませるように勧めてください.公開したくないデータを含んだ.Rhistoryをうっかり公開してしまうようなことは容易に起こります.

ビネット

認証が必要なコードをビネットに含めるのはやっかいなことです.読者自身のクレデンシャルをパッケージにバンドルしたくはないでしょうから!とはいえ,Jenny Bryanのアイディアによれば,ビネットはローカルでビルドされて,CRANではチェックされるだけだという事実を利用することができます:

セットアップのチャンクで以下のようにしてください:

NOT_CRAN <- identical(tolower(Sys.getenv("NOT_CRAN")), "true")
knitr::opts_chunk$set(purl = NOT_CRAN)

それから必要な箇所のチャンクオプションでeval = NOT_CRANとしてください.

付録:APIキーのベストプラクティス

読者のパッケージがAPIキーかトークンに基づく認証をサポートしている場合には,ユーザにはキーやトークンを環境変数に保存するよう勧めてください.Githubのバージョン3のAPIのラッパーであるrgithub を例にとって説明しますので,このテンプレートを読者のAPIとパッケージに合うように変更して,README.mdやビネットに含めるようにしてください.

  1. githubの設定のPersonal access tokens areaでパーソナルアクセストークンを作成して,トークンをクリップボードにコピーしてください.

  2. ホームディレクトリをを特定してください.分からない場合はRコンソールでnormalizePath("~/")と打ち込んでください.

  3. 新しいテキストファイルを作成してください.RStudioを使っている場合は File > New File > Text file を選択すればよいです.

  4. 以下のような行を作成してください:

    GITHUB_PAT=blahblahblahblahblahblah
    

    GITHUB_PATは,このAPIがどのサイトのものか思い出させるものです.blahblahblahblahblahblahにはクリップボードにあるパーソナルアクセストークンを貼り付けます.

  5. ファイルの最終行が空行になっていることを確かめてください(そうなっていない場合,Rは無言でファイルのロードに失敗します.行番号が表示されるエディタを使っている場合は,ファイルには2行存在して,2行目は空になっているべきです.)

  6. このファイルをホームディレクトリに.Renvironという名前で保存してください.ドット.で始まるファイルを使用したいのかと質問された場合は,YESとしてください.

    ふつうデフォルトではドットファイルは隠しファイルになっていますが,RStudioのファイルブラウザでは.Renvironが表示されるので,あとで編集し直すのも簡単です.

  7. Rを再起動してください..RenvironファイルはR セッションの最初にのみ処理されます.

  8. トークンにアクセスするにはSys.getenv()を使ってください.たとえば,GITHUB_PATrgithubパッケージで使うには以下のようにします:

    library(rgithub)
    ctx <- create.github.context(access_token = Sys.getenv("GITHUB_PAT"))
    # 他のパッケージ関数を使ってイシューを開いたりしてみてください.
    

FAQ: どうして.bash_profile.bashrcではなく.Renvironで環境変数を定義するのでしょうか?

多くのOSとR実行環境の組み合わせにおいて, .Renviron を使う方法はとりあえず上手く動きますが,bash関連のファイルではそう上手くはいかないからです.たとえばRがEmacsやRStudioの子プロセスである場合には,環境変数がRに渡されることが常に期待できるわけではありません.R専用のスタートアップファイルに環境変数を書いておくことで苦労が省けるのです.