PythonでSlackに通知する

# 目的
Pythonでslackに通知する
※slackの自身のユーザーで、slackチャンネルにメッセージを送る

# 背景
[前回](https://qiita.com/igapon1/items/21084182c3d159becd58)PythonでLINEに通知しました。
しかし無料だと月に200通までの制限があり、微妙な気持ちになりました。
slackならたくさん送れると思ったので作ってみました。

# 環境
- Windows11 バージョン24H2(OSビルド26100.2454)
 - Python3.12.4

# 作り方
1. [slackページ](https://api.slack.com/apps)でログインして、Create New Appを押下する
1. Create an appで「from scratch」を選択する
1. App Name と workspace を選択し，Create Appを押下する
1. Featuresの「OAuth & Permissions」を押下する
1. Scopesの「User Token Scopes」で「Add an OAuth Scope」を押下する
1. 「chat:write」を選択する
1. Settingsの Install Appを選択し、Install to <ワークスペース名>を押下する(以下の画像には私のワークスペース名であるRingが表示されている)
1. 許可するを押下する
1. アクセストークンを取得する
1. PythonのSlack通知クラスを作成する
1. slack-sdkをインストールする

## [slackページ](https://api.slack.com/apps)にアクセスして、Create New Appを押下する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/b27640df-1e4f-5b3e-00df-a94e910b7345.png)
## Create an appで「from scratch」を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/b5aeea7e-0876-9ba6-c4f3-d308080bb6cf.png)
## App Name と workspace を選択し，Create Appを押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/045b9555-66bf-0aaa-bb67-8d2e3ac93f2d.png)
## Featuresの「OAuth & Permissions」を押下
※以下の画面イメージは、App Nameをslack notificationで作った場合の例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/7fa3157d-3d8d-195f-be6b-dec84c15dc59.png)
## Scopesの「User Token Scopes」で「Add an OAuth Scope」を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/3612774e-c1cd-4ef5-ae7a-48e0d8fa3319.png)
## 「chat:write」を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/49ddf281-5772-58e8-584b-59cb392dd297.png)
## Settingsの Install Appを選択し、Install to <ワークスペース名>を押下
アプリをインストールするワークスペースのボタンが表示されるのでそれを押下する
※以下の画面イメージはworkspaceがRingの場合の例です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/581794a1-66b1-235a-b24a-a7103adc989f.png)
## 許可するを押下
※以下の画面イメージはworkspaceがRingの場合の例です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/65af1d41-e09c-d734-c5e8-b4db4560a269.png)
## アクセストークンを取得する
Settingsの Install Appで、User OAuth Tokenのアクセストークンを控える
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/d00d6b59-f659-e8bb-6d57-64a4c50f2383.png)
## PythonのSlack通知クラスを作成する
例えば、AIさんに以下を質問する([前回](https://qiita.com/igapon1/items/21084182c3d159becd58)記事で作った、LINEの通知を送るクラスは、こちら[line_message_api.py](https://github.com/igaponr/training/blob/1.10.0/python/Web_scraping/line_message_api.py))
```
python言語で、以下はLINEに通知を送るクラスですが、これと同様な機能でslackに通知を送るクラスを作ってください。
import json
from dataclasses import dataclass, asdict
import os
from typing import Optional
from argparse import ArgumentParser
from linebot import LineBotApi, WebhookHandler  # pip install line-bot-sdk
from linebot.exceptions import InvalidSignatureError, LineBotApiError
from linebot.models import TextSendMessage
# 警告抑制
import warnings
from linebot import LineBotSdkDeprecatedIn30


@dataclass(frozen=True)
class LineMessageAPI:
    """
    LINE Messaging API のアクセストークンとChannel Secretを保持し、
    メッセージ送信機能と、設定情報のJSONファイル保存・ロード機能を提供するクラス。
    デストラクタで設定情報をJSONファイルに保存します。
    設定情報指定せずにインスタンス作成時は、JSONファイルがあればロードし、
    JSONファイルがなければエラー表示で終了します。
    """
    access_token: str
    channel_secret: str
    json_path: str = "line_message_api_config.json"
    line_bot_api: LineBotApi = None  # 型ヒントを追加
    handler: WebhookHandler = None  # 型ヒントを追加

    def __post_init__(self):
        """
        アクセストークンとChannel Secretが指定されていない場合、JSONファイルからロードを試みる。
        JSONファイルが存在しない場合は、エラーメッセージを表示して終了する。
        """
        if not self.access_token or not self.channel_secret:
            try:
                loaded_self = self.load_from_json()
                # _replace で frozen な dataclass の属性を更新
                object.__setattr__(self, "access_token", loaded_self.access_token)
                object.__setattr__(self, "channel_secret", loaded_self.channel_secret)
            except FileNotFoundError:
                print(f"エラー: JSONファイル({self.json_path})が見つかりません。"
                      "アクセストークンとChannel Secretを指定するか、JSONファイルを作成してください。")
                exit(1)
        self.save_to_json()
        object.__setattr__(self, "line_bot_api", LineBotApi(self.access_token))
        object.__setattr__(self, "handler", WebhookHandler(self.channel_secret))

    def send_message(self, user_id: str, message: str) -> bool:
        """
        指定したユーザーにメッセージを送信する。

        Args:
            user_id: 送信先のユーザーID。
            message: 送信するメッセージ。
        Returns:
            送信に成功した場合はTrue、失敗した場合はFalseを返す。
        """
        text_message = TextSendMessage(text=message)
        try:
            self.line_bot_api.push_message(user_id, text_message)
            return True  # 送信成功
        except LineBotApiError as e:
            print(f"メッセージの送信に失敗しました: {e}")
            return False  # 送信失敗

    def save_to_json(self) -> None:
        """
        設定情報をJSONファイルに保存する。
        """
        try:
            with open(self.json_path, 'w', encoding='utf-8') as f:
                json.dump(asdict(self), f, ensure_ascii=False, indent=4)
        except Exception as e:
            print(f"メッセージの保存に失敗しました: {e}")

    @classmethod
    def load_from_json(cls, json_path: Optional[str] = None) -> "LineMessageAPI":
        """
        JSONファイルから設定情報を読み込み、インスタンスを生成する。

        Args:
            json_path: JSONファイルのパス。指定しない場合は、デフォルトのパスを使用する。

        Returns:
            LineMessageAPI インスタンス。
        """
        path = json_path or cls.json_path
        if os.path.exists(path):
            with open(path, "r") as f:
                data = json.load(f)
                return cls(**data)
        else:
            raise FileNotFoundError(f"JSONファイルが見つかりません: {path}")


def get_option():
    arg_parser = ArgumentParser()
    arg_parser.add_argument('-a', '--access_token', type=str, default="",
                            help='LINE Messege APIのアクセストークンを指定する')
    arg_parser.add_argument('-c', '--channel_secret', type=str, default="",
                            help='LINE Messege APIのチャンネルシークレットを指定する')
    arg_parser.add_argument('-i', '--user_id', type=str, default="",
                            help='メッセージを送るUserIDを指定する')
    arg_parser.add_argument('-m', '--message', type=str, default="Hello from Python!",
                            help='送るメッセージを指定する')
    return arg_parser.parse_args()


if __name__ == '__main__':  # インポート時には動かない
    # 警告抑制 v3 api
    warnings.filterwarnings("ignore", category=LineBotSdkDeprecatedIn30)
    arg = get_option()
    print(arg)
    if arg.access_token:
        line_message_api = LineMessageAPI(access_token=arg.access_token, channel_secret=arg.channel_secret)
    else:
        line_message_api = LineMessageAPI(access_token="", channel_secret="")
    if line_message_api.send_message(arg.user_id, arg.message):
        print("メッセージを送信しました。")
    else:
        print("メッセージの送信に失敗しました。")
```
回答のコードを、自分好みに直して使います。
[slack_message_api.py](https://github.com/igaponr/training/blob/1.11.0/python/Web_scraping/slack_message_api.py)
## slack-sdkをインストールする
コマンドラインで以下を入力する
```
pip install slack-sdk
```

# 使い方
SlackMessageAPIクラスの[docstring](https://github.com/igaponr/training/blob/90542252f5d01281e0b979862b54ccc11c05790c/python/Web_scraping/slack_message_api.py#L12C1-L17C8)を見てもらえばわかりますが、引数access_tokeにアクセストークンを指定して、SlackMessageAPIクラスのインスタンスを作り、引数channel_idで送るチャンネルのIDと、messageで送るメッセージを指定して、send_messageメソッドを呼び出せば通知できます。
初回は、以下のコマンドで通知を送ります。slack_message_api_config.jsonファイルにアクセストークンが保存されます。
```
python slack_message_api.py -a <アクセストークン> -c <通知を送るチャンネルID> -m <メッセージ>
```
次回からは、アクセストークンをslack_message_api_config.jsonから読み込むので、以下の通り省略できます。
```
python slack_message_api.py -c <通知をチャンネルID> -m <メッセージ>
```
チャンネルIDを指定しなかった場合のデフォルトは「#general」です。

# おわりに
AIさんのおかげで、すぐ作れました。
Slack APIにも、1つのワークスペースにつき 1 時間あたり 30,000 回のイベント送信というレート制限があるらしいですが、私の使い方だとまず引っかからないので、今後も活用したいです。

# 関連リンク
## 参考サイト
- [Python で Slack API や Webhook を扱うなら公式 SDK（slack-sdk/slack-bolt）を使おう](https://qiita.com/seratch/items/8f93fd0bf815b0b1d557)
- [スケーラブルな Slack アプリ開発: API レート制限の処理](https://api.slack.com/lang/ja-jp/rate-limit)
- [Python Slack SDKプロジェクトの説明](https://pypi.org/project/slack-sdk/)

## ドキュメント
- [Python Slack SDK](https://tools.slack.dev/python-slack-sdk/)