技術書を読むためのカンバン設計と実装

# 概要

たくさん読みたい本がある人にとって、カンバンを使って本の管理をすると
どの本がどの状態なのかパッと見るだけでわかって良いです。

ここではカンバンを使った本の管理を紹介します。

便利なカンバンツールとして **Trello** があります。
実際にそれを使って管理している筆者の例を載せておきました（以下）。

![カンバンで本の管理](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/b2304374-6d56-ee99-81a7-1e43bbbcc55d.png)

APIを使った連携もしているので、そちらも説明できたらと思います。

# カンバンの作り方

## 自分がどんなふうに本を読みたいか考える

自分らしい読み方が人それぞれあると思います。
最初にそれを考えます。

筆者の場合は

1. インターネットや書籍、図書館で読みたい本が見つかる。
1. 読みたい本について購入するか、図書館で借りるか検討する。
1. 購入または借りる。
1. 読む。
1. 読み終わる（読みたかった部分だけ読む）。
1. または自分に合わず途中で読むのを止める。
1. 面白ければ読書感想文を書く。

みたいな感じでした。

## 本に対する感情を抽出する

読み方が洗い出されれば、自然とカンバンの要素が見えてくるはずです。ここでは **本** に対して抱いている感情に注目します。

![本への感情.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/c9b87481-c3eb-f62a-c24f-58af0550ddce.png)

## カンバンを設計する

感情をわかり易い言葉へ置き換えていきます。

* これほしいな～、興味あるな･･････ ＞＞＞　読みたい本の候補
* 買った！　後で読もう　＞＞＞　まだ読んでいない本
* 今読み始めた、半分くらいまで　＞＞＞　読んでいる本
* 読み終わったー　＞＞＞　読んだ本

のような感じで筆者は変換しました。

これらの言葉をカンバンのタイトルにします。
そうすると、こんな感じになります。

![カンバン.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/ff465592-5da6-0085-e760-67cee3017f1a.png)

各カンバンの順序は左から右へ違和感がないように並べれば大丈夫です。

## 好きなカンバンツールで実装する

最後に設計したカンバンを実装します。
筆者はTrelloで作りました。

# 付録

## カーリルAPI

カーリルが提供している図書館APIというものを使うことで、ISBNから任意の図書館の書籍情報を取得できます。
[図書館 API](https://calil.jp/doc/api.html)

例えば、近所の図書館で読みたい本が借りられるかどうかを簡単にチェックできるのです。

筆者は図のような使い方をしています。

![API連携.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/302006/9e4a8dce-e1e0-a6c7-e466-870794c6e621.png)

## 実際のプログラム

Pythonで実装したプログラムを記載します（API連携部分だけ抜粋しています）。

上記図とプログラムの対応。

| 図の番号 | 行っていること | プログラム中の関数 |
| ---- | ---- | ---- | 
| ➂ | Trelloのカード情報を取得 | trelloapi.get_cards_from_board() |
| ➃ | 近隣図書館の蔵書検索 | libraryapi.get_is_in_library() |
| ➅ | 図書館に所蔵されているかどうかでラベルを貼る | add_label_to_card() |



これは図書館APIに関する部分。

```python:libraryapi.py

import json
import logging
import time
import requests

import config


def get_is_in_library(isbn: str) -> bool:
    """
    Parameters
    ----------
    isbn: str
        本のISBN
    Returns
    -------
    is_in_library: bool
        図書館にあるかどうか
    """
    payload: dict = {
        "appkey": config.rule_file["calili-api-key"]["appkey"],
        "isbn": isbn,
        "systemid": config.rule_file["library"]["systemid"],
        "callback": "no"
    }

    r = requests.get("https://api.calil.jp/check", params=payload)
    logging.info(f"リクエスト {get_json_from_response(r)}")
    while not validate_response(r):
        payload_of_re_request = {
            "session": get_json_from_response(r)["session"],
        }
        # カーリルAPIの仕様として再リクエストまで2秒空ける
        time.sleep(2)
        r = requests.get("https://api.calil.jp/check", params=payload_of_re_request)
        logging.info(f"リクエスト {get_json_from_response(r)}")

    is_in_library: bool = len(
        get_json_from_response(r)["books"][payload["isbn"]][payload["systemid"]]["libkey"].keys()
    ) > 0
    return is_in_library


def validate_response(response: requests.Response) -> bool:
    """
    カーリルの図書館APIから返ってきたデータを検証するメソッド。
    continueという値が1の場合は、2秒以上の間隔を空けて再度リクエストを送る。
    Parameters
    -----------
    response: requests.Response
    Return
    -------
    is_valid: bool
    """
    is_valid: bool = False
    if get_json_from_response(response)["continue"] == 0:
        is_valid = True

    return is_valid


def get_json_from_response(response: requests.Response) -> dict:
    """
    カーリルAPIからのcallbackという文字列が含まれているレスポンスをJson形式に変換しようとすると、デコードエラーが発生する。
    そのため、テキスト形式で取得した後、Json形式へ変換する。
    callbackが含まれていない場合は、responseライブラリに実装されているメソッドを使う。
    Parameters
    ----------
    response: requests.Response
    Returns
    -------
    dict
    """
    if "callback" in response.text:
        return json.loads(response.text.split("(")[1].split(")")[0])
    else:
        return response.json()

```

こちらがTrelloAPIに関する部分

```python:trelloapi.py
import requests

import config


def add_label_to_card(card_id: str) -> None:
    """
    Trello のカードにラベルを付与する。
    Parameters
    -----------
    card_id: str
        TrelloのカードのID
    Returns
    --------
    None
    """
    label_payload: dict = {
        "value": config.rule_file["trello-book-board"]["label-id"],
        "key": config.rule_file["trello-api-key"]["key"],
        "token": config.rule_file["trello-api-key"]["token"]
    }

    requests.post(f"https://api.trello.com/1/cards/{card_id}/idLabels", data=label_payload)


def get_cards_from_board() -> list:
    """
    Trelloの「読みたい本の候補」リストにあるカードを返すメソッド
    Returns
    --------
    r.json(): list
        Json形式のカード情報
    """
    book_list_id: str = config.rule_file["trello-book-board"]["list-id"]
    payload: dict = {
        "key": config.rule_file["trello-api-key"]["key"],
        "token": config.rule_file["trello-api-key"]["token"]
    }

    r = requests.get(f"https://api.trello.com/1/lists/{book_list_id}/cards", params=payload)
    return r.json()
```