JupyterをRESTから操作する

### はじめに
JupyterNotebookはブラウザで操作できるので、普通はRESTで操作できる必要はないです。

個人用の備忘録なのですが、こんなのが役に立つのは、Jupyterを使ったシステムを作っているか、
それらの保守等で、自動構築やテスト自動化などが必要な人たちですね。
なんて、マニアックなんだ。

### 前提
まず、Jupyter Notebook の バージョンは 5.7.2 となる。
色々試してみたが、どうにもパスワード認証がうまくいかなくて、token で動かすとうまくいった。

### トークンを取ってくる
token とは、[Security in the Jupyter notebook server](https://jupyter-notebook.readthedocs.io/en/stable/security.html)のページに登場する `token=` の後に続く文字列の事です。

```
[I 11:59:16.597 NotebookApp] The Jupyter Notebook is running at:
http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
```

### ファイルの一覧
token指定の所が肝。
この指定ができれば、あとはAPIの流儀に従ってパラメータやパス指定するだけ、とても簡単です。

```python
from __future__ import print_function
from __future__ import unicode_literals
import argparse
import json
import requests

def main():
    """main."""
    parser = argparse.ArgumentParser()
    parser.add_argument('base_url', help='base_url')
    parser.add_argument('token', help='token')
    parser.add_argument('relpath', nargs='?', default='/', help='relative path')
    args = parser.parse_args()

    headers = {
        'Authorization': 'token %s' % args.token,
        'Content-Type': 'application/json'
    }
    resp = requests.get(
        '{}/api/contents{}'.format(args.base_url, args.relpath),
        headers=headers
    )
    print("{}".format(json.dumps(resp.json(), indent=4)))

if __name__ == '__main__':
    main()
```

ちなみに、パスワードを使おうとして 'Authorization': 'Basic %' … みたくやったのですが、
403 地獄から逃れられず、レスポンスは json でなくて html のページに飛ばされた。

### アップロード
- XSRF のエラーが出る場合は、requests でセションで cookie とか持ちまわるように小細工が必要。
- 画像とかを送りたいときは、 base64 で埋め込んでやれば良い
- URLの相対パス指定が肝でファイルやディレクトリがないと失敗する  
  そういう意味で、パラメータ側の name とか path の存在意義がいまいちわからず

```python
from __future__ import print_function
from __future__ import unicode_literals
import argparse
import json
import requests

def main():
    """main."""
    parser = argparse.ArgumentParser()
    parser.add_argument('base_url', help='token')
    parser.add_argument('token', help='token')
    parser.add_argument('relpath', help='relative path ... ex. /foo/bar.txt')
    args = parser.parse_args()

    api_url = '{}/api/contents{}'.format(args.base_url, args.relpath)
    headers = {
        'Authorization': 'token %s' % args.token,
        'Content-Type': 'application/json'
    }
    req_body = {
        'name': 'bar.txt',
        'path': '.',
        'type': 'file',
        'format': 'text',
        'content': 'foo'
    }
    resp = requests.put(
        api_url,
        json.dumps(req_body).encode("utf-8"),
        headers=headers
    )
    if resp.headers.get('Content-Type').startswith('application/json'):
        print("{}".format(json.dumps(resp.json(), indent=4)))
    print("")

if __name__ == '__main__':
    main()
```

### 参考
- [Security in the Jupyter notebook server — Jupyter Notebook 5.7.4](https://jupyter-notebook.readthedocs.io/en/stable/security.html)  
  token の取り方とか。
- API
  - [Jupyter Notebook Server API](https://github.com/jupyter/jupyter/wiki/Jupyter-Notebook-Server-API#Notebook-and-file-contents-API)  
  `The schema is here` と書かれているあたりをみれば何となく分かる。もし、自由に好きなパッケージをインストールできる状況下にあれば、 requests を使わなくても、Swagger (Open API)で生成してクライアントI/Fを配備しても良いですけどね。
  - [Contents API](https://jupyter-notebook.readthedocs.io/en/stable/extending/contents.html)
- [Cookie passing for Jupyter Notebook APIs #1431](https://github.com/jupyterhub/jupyterhub/issues/1431)  
  バージョンや環境によっては、`X-XSRF-Token`をいれないと弾かれることがある。クロスサイト リクエスト フォージェリ (CSRF/XSRF) の対策で、クロスサイトかどうかを判別するトークンをいれないといけないとか何とか。