スプレッドシートを設定ファイルとして使うライブラリを作ってみた

github.com

slackやdiscordなどのbotを作るときに、ファイルを触れない人にも設定をいじってもらおうとしたら、スプレッドシートあたりが無難なのかなと思いました。そんなわけでスプレッドシートを設定ファイルとして使うライブラリを作ってみました。

インストール方法

いつもどおりpipで以下のように実行してみてください (主にgspreadとpydanticに依存しています)。

$ pip install sheetconf

使い方

基本的には pydantic で作成したConfigクラスをパラメータとして取ります。

sheetconf.loadfile() にConfigクラスとformatを渡してください。

注意点として、必ず、セクションが別れたConfigクラスを定義する必要があります。以下のコードでは、xxx,yyy,loggerというセクションに分かれています。

import typing_extensions as tx
import sheetconf
from pydantic import BaseModel, Field


class XXXConfig(BaseModel):
    name: str
    token: str


class YYYConfig(BaseModel):
    name: str = Field(default="yyy", description="name of yyy")
    token: str = Field(description="token of yyy")


class LoggerConfig(BaseModel):
    level: tx.Literal["DEBUG", "INFO", "WARNING", "ERROR"]


class Config(BaseModel):
    xxx: XXXConfig
    yyy: YYYConfig
    logger: LoggerConfig


if __name__ == "__main__":
    url = "https://docs.google.com/spreadsheets/d/1PgLfX5POop6QjpgjDLE9wbSWWXJYcowxRBEpxmpG8og"
    config = sheetconf.loadfile(url, config=Config, format="spreadsheet", adjust=True)

    print(config)
    # Config(logger=LoggerConfig(level='INFO'), xxx=XXXConfig(name='xxx', token='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'), yyy=YYYConfig(name='yyy', token='yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'))

利用しているスプレッドシートはこういう感じのものです。スプレッドシート中の各ワークシート(タブ)が1つのセクションを表しています。

xxx yyy logger

credentials

スプレッドシートの読み取りには権限が必要です。初回実行時にこういうメッセージが出るのでいい感じに設定してください。

CredentialsFileIsNotFound('/Users/me/.config/sheetconf/credentials.json')
        Please save file at /Users/me/.config/sheetconf/credentials.json (OAuth 2.0 client ID)
        opening... https://console.cloud.google.com/apis/credentials

CSVファイルを設定ファイルとして読み込み

何かを試すときに常にスプレッドシートが必要というのもまた面倒ですね。formatに"csv"を指定すると、csvファイルをスプレッドシートの代わりに読み込めます。

$ tree config-csv/
config-csv/
├── logger.csv
├── xxx.csv
└── yyy.csv

$ python -m sheetconf.cli load --config=00sheetconf.py:Config --format=csv config-csv
Config(xxx=XXXConfig(name='xxx', token='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'), yyy=YYYConfig(name='yyy', token=''), logger=LoggerConfig(level='DEBUG'))

もちろんpython側のほうでも同様の指定でCSVを読み込めます。

if __name__ == "__main__":
    url = "config-csv"
    config = sheetconf.loadfile(url, config=Config, format="csv", adjust=True)
    print(config)

対応しているのは以下の様なフォーマットです。

name,value,value_type,description
name,xxx,str,
token,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,str,

JSONファイルの読み込み

たとえスプレッドシートが不要になったとしても、CSVファイルの場合は1ファイルで完結しないので不便ですね。JSONファイルでも大丈夫です。

if __name__ == "__main__":
    url = "config.json"
    config = sheetconf.loadfile(url, config=Config, format="json", adjust=True)

    print(config)

以下の設定ファイルはlogger部分を含んでいませんが、loggerはConfigの定義時にdefaultを指定しているので推測してくれます。

config.json

{
  "xxx": {
    "name": "",
    "token": ""
  },
  "yyy": {
    "name": "yyy",
    "token": ""
  }
}
$ python -m sheetconf.cli load --config=00sheetconf.py:Config --format=json config.json
Config(xxx=XXXConfig(name='', token=''), yyy=YYYConfig(name='yyy', token=''), logger=LoggerConfig(level='DEBUG'))

init

初回などに設定ファイルを作るのは面倒ですね。シートさえ用意してあげたらinitで生成することができます。面倒なのでJSONでやりますが、CSVファイルやスプレッドシートでも同様です。

$ python -m sheetconf.cli init --config=00sheetconf.py:Config --format=json config2.json
$ cat config2.json
{
  "xxx": {
    "name": "",
    "token": ""
  },
  "yyy": {
    "name": "yyy",
    "token": ""
  },
  "logger": {
    "level": "DEBUG"
  }
}

jsonschema

ついでにおまけと言ってはなんですが、pydanticのJSONSchemaを生成する機能をCLI越しで使えます。

$ python -m sheetconf.cli schema --config=00sheetconf.py:Config
{
  "definitions": {
    "XXXConfig": {
      "title": "XXXConfig",
      "type": "object",
      "properties": {
        "name": {
          "title": "Name",
          "type": "string"
        },
        "token": {
          "title": "Token",
          "type": "string"
        }
      },
      "required": [
        "name",
        "token"
      ]
    },
    "YYYConfig": {
      "title": "YYYConfig",
      "type": "object",
      "properties": {
        "name": {
          "title": "Name",
          "description": "name of yyy",
          "default": "yyy",
          "type": "string"
        },
        "token": {
          "title": "Token",
          "description": "token of yyy",
          "type": "string"
        }
      },
      "required": [
        "token"
      ]
    },
    "LoggerConfig": {
      "title": "LoggerConfig",
      "type": "object",
      "properties": {
        "level": {
          "title": "Level",
          "anyOf": [
            {
              "const": "DEBUG",
              "type": "string"
            },
            {
              "const": "INFO",
              "type": "string"
            },
            {
              "const": "WARNING",
              "type": "string"
            },
            {
              "const": "ERROR",
              "type": "string"
            }
          ]
        }
      },
      "required": [
        "level"
      ]
    },
    "Config": {
      "title": "Config",
      "type": "object",
      "properties": {
        "xxx": {
          "$ref": "#/definitions/XXXConfig"
        },
        "yyy": {
          "$ref": "#/definitions/YYYConfig"
        },
        "logger": {
          "$ref": "#/definitions/LoggerConfig"
        }
      },
      "required": [
        "xxx",
        "yyy",
        "logger"
      ]
    }
  },
  "$ref": "#/definitions/Config"
}

gist