ErrbotのStorageプラグインを作ってみる

興味本位で、ErrbotのStorageプラグインを１個作ってみた話です。
以降、このページのコードはPython3.6系のみで動きます。[^1]



# Storageプラグインの役割

Errbotのプラグインが、「永続化したいデータを管理するためのプラグイン」です。
何も指定しなければ、ビルトインの`Shelf`が使われます。
サードパーティ製のプラグインでは、SQLAlchemyを経由してRDBMSで管理する`err-storage-sql`、Firebaseを使う`err-storage-firebase`などなどがあります。詳しくは[ErrbotのGithubプロジェクトページにあるWiki](https://github.com/errbotio/errbot/wiki)を見てみましょう。[^2]

# 作る前に

## Storageプラグインの使われ方

### 読み込みまで

プラグインが探索されるタイミングは起動時のみです。
また、設定上の`BOT_EXTRA_PLUGIN_DIR`ではなく`BOT_EXTRA_STORAGE_PLUGINS_DIR`を追加の探索対象とします。
そこからプラグインマネージャーが認識するまでは、他のプラグインと同じです。

### 初期化するまで

Storageプラグイン自体の仕事は、コアプラグイン・Botプラグインが起動するタイミングで、ストレージを生成することです。
それ以降は（多分）何もしません。

### Errbot稼働中

[以前に書いた記事でざっくり説明した](https://qiita.com/attakei/items/e77986da9bb311d5992b)通り、Botプラグインが永続化させたいデータに対するやり取りは、オーバライドされている辞書アクセス用メソッドを介して行われます。[^3]
この際の最終アクセス先がストレージとなります。

```py3:some_plugin.py
from errbot import BotPlugin, botcmd

class SomePlugin(BotPlugin):
   @botcmd
   def like(self, msg, args):
       self['like'] = args  # <- ここで使われる
       return f"OK! You like {args}. I remembered"
```

### Errbot終了

ストレージが持っている終了時に呼ばれるメソッドが呼ばれて役目を終えます。[^4]

## ファイル構造について

普通のプラグインと同様に、`****.plug`ファイルとplugファイルが参照している`***.py`が必要になります。
この辺は、`yapsy`を使っているからか、BotプラグインもBackendプラグインもStorageプラグインも同じです。

```restructuredtext:file_list.rst
* 必須
  * json.plg
  * jsonpg.py
* 任意
  * README.md
  * requirements.txt
  * LICENSE
```

`requirements.txt`には依存ライブラリが書かれていることが多いのですが、起動時にこのファイルがあったら自動で`pip install`をすることが無いです。
ので、面倒だったらなくてもいいかもしれません。


# 作ってみる

## プラグインの中身

[errbot.storage.base](https://github.com/errbotio/errbot/blob/master/errbot/storage/base.py)内にある`StorageBase`,`StoragePluginBase`を継承して`@abstractmethod`でデコレートされているメソッドを全部実装すればいいです。

### Pluginクラス

この３条件を満たしていればいいです。

* `XXXPlugin`という形式のクラス名であること（XXXにはplugファイルで定義したNAMEが入ります）
* `__init__`が引数を１個受け取ること
* `open()`が引数を１個受け取り、ストレージ本体のインスタンスを返すこと（以降、このインスタンスはStorageクラスとします）

### Storageクラス

* 次のメソッドが実装されていること
    * `set`: キーと値を受け取る
    * `get`: キーを受け取り、値を返す
    * `remove`: 指定したキーの値を削除する
    * `len`: キーの数を返す
    * `keys`: キーのリストを返す
    * `close`: 保存する

# 出来上がったものがこちら

「きちんと動くと思うけど、端折れるところは端折ってる」感じで記載します。

なお、個人的にはYAMLのほうが手っ取り早く見やすくできたので、[YAMLストレージ](https://github.com/attakei/err-storage-yaml)も作ってます。

```ini:json.plug
[Core]
Name = JSON
Module = jsonpg

[Documentation]
Description = JSON save/load storage for Errbot
```

```py3:jsonpg.py
import os, json
from errbot.storage.base import StorageBase, StoragePluginBase


class JSONStorage(StorageBase):
    def __init__(self, save_path):
        self.save_path = save_path
        try:
            with open(self.save_path) as fp:
                self.data = json.load(fp)
        except FileNotFoundError:
            self.data = {}

    def set(self, key, value):
        self.data[key] = value

    def get(self, key):
        return self.data[key]

    def remove(self, key):
        del self.data[key]

    def len(self):
        return len(self.keys())

    def keys(self):
        return self.data.keys()

    def close(self):
        with open(self.save_path, 'w') as fp:
            json.dump(self.data, fp)


class JSONPlugin(StoragePluginBase):
    def __init__(self, bot_config):
        super().__init__(bot_config)
        # 保存先はShelfプラグイン等と同じ場所
        self.data_dir = bot_config.BOT_DATA_DIR

    def open(self, namespace):
        # 各プラグインごとに呼ばれるので、namespaceをファイル名に使う
        save_path = os.path.join(self.data_dir, f"{namespace}.json")
        return JSONStorage(save_path)
```

# 最後に

昨年に[Backendプラグインの記事](https://qiita.com/deko2369/items/f70102da66370215f311)を書いてる方がいましたが、Storageプラグインは考慮したほうがいい点がやや少ないので作りやすいですね。
とはいえ、保存先の仕様をちゃんと把握しないといけないので、楽とまでは言えないですが。

# 参考

* 公式のドキュメントと、ビルトインのクラス
    * http://errbot.io/en/latest/user_guide/storage_development/index.html
    * https://github.com/errbotio/errbot/tree/master/errbot/storage
* SQLプラグイン（プラグインプロジェクトの構造についての参照元）
    * https://github.com/errbotio/err-storage-sql


[^1]: f-stringを直せば、Python3系ならだいたい動くはずです
[^2]: 定期的に更新されています
[^3]: https://github.com/errbotio/errbot/blob/master/errbot/storage/__init__.py
[^4]: もちろん、実装によっては即時保存されることもあります。