自作Packageで共通処理を切り出してpip installで複数プロジェクトで使い回す(Python, Hatchling)

# 背景
- 複数のアプリで利用される共通処理を自作Packageで切り出して利用したい
- 以下のようにリポジトリ内に複数アプリから共通して使われるパッケージを用意します
- common_slackという自作パッケージを用意します

```
.
├── README.md
├── common
│   ├── common_slack
│   │   ├── __init__.py
│   │   └── slack.py
├── app_A
│   ├── Dockerfile
│   ├── main.py
└── app_B
    ├── Dockerfile
    ├── main.py
```
- Python, Pipenv, Hatchlingを使った環境で進めていきます

### なんでわざわざパッケージ化するの？ 普通にimportじゃダメなの？
- Dockerコンテナ化するときに、app_A, app_Bの外側のディレクトリを考慮するのが面倒なので、build済みwheelを対象プロジェクトに配置して、pip installで管理できるようにしていきます

# Hatchling? SetupTools?
BuildのBackendシステムとしてはHatchlingを使います. SetupToolsも有名ですが、よりモダンなBuildのBackendシステムということでhatchlingを採用しました.

用語を整理していきます.

https://packaging.python.org/en/latest/glossary/#term-Build-Backend


#### Build Backend
source tree や source destricutionを受け取り、wheelやsource districutionをビルドするライブラリ. 例: Hatchling, Setuptools

#### Build Frontend
ユーザーがwheelやsource distributionをビルドするために使うツール. 実際のビルドはBuild Backendに委譲する. 例: pip, build

# 手順

- 1 . packageの実装
- 2 . packageのビルド
  - pyproject.tomlの用意
  - build
- 3 . projectAからの呼び出し

最終的に以下のようなディレクトリ構成になります.

```
.
├── common
│   ├── common_slack
│   │   ├── __init__.py
│   │   └── slack.py
│   ├── dist
│   │   ├── common_slack-0.0.1-py3-none-any.whl
│   │   └── common_slack-0.0.1.tar.gz
│   ├── pyproject.toml
│   ├── requirements.txt
├── projectA
│   ├── Dockerfile
│   ├── main.py
│   ├── requirements.txt
└── projectB
    ├── Dockerfile
    ├── main.py
    ├── requirements.txt
```

## 1. packageの実装

まずは `common/common_slack/slack.py` の実装

```py
import requests
import json

class Slack:

    def post_message(self, title: str, message: str, channel_id: str) -> None:
        requests.post(
            url='https://slack.com/api/chat.postMessage',
            headers={
                'Authorization': f'Bearer xxx',
                "Content-Type": "application/json"
            },
            data=json.dumps({
                'channel': channel_id,
                "attachments": [
                    {
                        "color": color,
                        "title": title,
                        "text": message,
                    }
                ]})
        )
        return
```

`common/common_slack/__init__.py` の実装

```py
from ai_sier_core.slack import Slack
__all__ = ('Slack')
```

## 2. packageのビルド

### pyproject.tomlの用意

`common/pyproject.toml` でpackageの設定をしていきます.

https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#dependencies-and-requirements

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "common_slack"
version = "0.0.1"
authors = [
  { name="Suzuki Shogo", email="xxx@jp" },
]
description = "common slack library"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = ['requests']
```

- [project].nameはpackageの名前です.  この名前と`-`packageの親ディレクトリのpathが一致している必要があります. また、nameが`common-slack` の場合は pathは `common_slack`で探索されるので注意してください.
- packageの依存関係を作りたい場合は、dependenciesに書いてください.

### build

```shell
cd common/common_slack
python3 -m pip install --upgrade build
python3 -m build
```

すると、`common/dist`以下に wheelの生成物である`common_slack-0.0.1-py3-none-any.whl` と source distributionの生成物である `common_slack-0.0.1.tar.gz` が出力されます.

## 3. projectAからの呼び出し

### pip installする

以下のコマンドで生成したwheelをinstallします

```shell
cd project_A
pip install ../common/dist/common_slack-0.0.1-py3-none-any.whl
# pipenvの場合
# pipenv run pip install ../common/dist/common_slack-0.0.1-py3-none-any.whl
```

```main.py
from common_slack import Slack

Slack().post_message('title', 'description')
```

これで共通処理を複数PJから呼び出せます.
Packageを更新した場合は、buildしなおして、呼び出し側がpip installしなおせば反映されます.

# 参考
https://packaging.python.org/en/latest/

https://packaging.python.org/en/latest/tutorials/packaging-projects/