【個人開発】Pythonライブラリを開発からリリースまでしてみた感想と備忘録

## 概要
久しぶりにPythonライブラリを開発、リリースしたのでその感想と備忘録を残していきます。

## お前だれ？
ブラジルでデータサイエンティスト/機械学習エンジニアやってます。この記事を書き始めてわかったけど、思ってた以上に日本語長文の書き方忘れてて焦ってます。ところどころ変な日本語にご勘弁。    
プライベートで開発してたサービスが一段落したので練習がてらPythonライブラリを開発、リリースまでしてみたので、その感想と備忘録を残していきます。

## 何作ったの？
今回作ったのはデータ管理のためのライブラリ。練習用とはいえ、想定ユーザーはデータサイエンティスト。  
データサイエンティストがタスクやプロジェクトで複数箇所からデータを取ってきて、クリーニング、バリデーション、タスクのためのデータセット作成を行うときにそれらのデータを手軽に管理できるライブラリを作ってみました。  

インストールは
```bash
pip install dcraft
```

リポジトリは以下
[![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=marubontan&repo=dcraft)](https://github.com/marubontan/dcraft)
## 使い方
以下の例はデータをRaw Layerにセーブ、ロードする。dcraftではデータをRaw layer、Trusted layer、Refined layerにセーブして管理します。それぞれの役割はこんな感じ。　　
* Raw Layer: ソースから取ってきたデータそのまんま
* Trusted Layer: クリーニングとバリデーション後のデータ
* Refined Layer: 個別のタスク、目的に応じたデータ

これらのデータを現在のバージョンだとローカルかGCP上にmetadataと共に保存していく感じ。

```python
import os
import pandas as pd
from dcraft import create_raw, LocalDataRepository, LocalMetadataRepository, read_layer_data

data = pd.DataFrame({"a": [1,2], "b": [None, 4]})
raw_layer_data = create_raw(
    data,
    "fake-project",
    "Shuhei Kishi",
    "This is fake project",
    {"version": "0.0.1"}
)


CURRENT_DIR = os.getcwd()
DATA_DIR_PATH = os.path.join(CURRENT_DIR, "data")
METADATA_DIR_PATH = os.path.join(CURRENT_DIR, "metadata")

data_repository = LocalDataRepository(DATA_DIR_PATH)
metadata_repository = LocalMetadataRepository(DATA_DIR_PATH)
raw_layer_data.save("parquet", data_repository, metadata_repository)


# loaded_raw_layer_data = read_layer_data("id-from-metadata", data_repository, metadata_repository)
```

## やったこと
### コーディング
基本的にはデータのストア、保存、ロードを管理するだけのライブラリなので内部アーキテクチャはシンプルに進めていった。interfaceとdomainに分けて開発していく感じ。最初はクリーンアーキテクチャのレイヤー構造にならって進めていこうと思っていたけど、ライブラリ開発における`use case layer`の勘所がわからなかったので今回は不採用。  
現時点でローカルとGCPのリソースに保存可能にしてるので、テストはユニットテストとインテグレーションテストの２つ。  
シンプルに始めようってことで常にあるbranchはmain branchのみ。機能追加やバグ修正はそこからbranch切ってmerge.  
### CI/CD
個人開発の味方、Github Actions、でテストとデプロイは自動化。大体の必要な機能はすでにプラグインが存在してるので、ググって使っていく感じで。  
### ドキュメンテーション
sphinxを使用。main branchにmergeしたらRead the Docsにデプロイ。  

## 案外きつかったこと
### 想定ユーザーに合わせたインターフェース
社内ツールはそこそこ作ってきているので気軽な気持ちで開発し始めたけど、OSSとなると想定ユーザーが使いやすく感じるのかという部分が気になって、結局、開発過程で結構な頻度で名前やパスを変更することになりました。  
『こっちのほうが綺麗だけどユーザー視点で面倒か？』みたいな思考と常に格闘する感じ。
### どこまでやる？
普段はビジネスが絡んだ文脈で開発してるので、ビジネス視点で必要な機能とクオリティを定義して開発プロセスを作ってきたけど、OSS開発だとちょっと途方に暮れたというのが実際のところ。  
最初のバージョンではライブラリのアイデンティティを維持するための機能とクオリティがあれば、他の部分は継続的に足して行けば良いわけだけど、結局その通りにはできなかった感じで、結局いくつかの機能を足してデプロイ後にあるべき物を作ってなかったことに気づいて『うーんっ』ってなってる。
具体的にはローカルへのデータ保存で最初のバージョンは多分十分だったのにGCPリソースの機能つけちゃった。でもって、データをオブジェクトにストアする段階でするべきバリデーションを忘れてた。  
### Sphinx
普段はドキュメンテーションはConfluence上でマニュアルでやってたので、そういえばSphinxを使うのは初めてだった。そこそこ長くデータサイエンティストやってるけど、使ったことなかったなーって気づいてちょっと驚いた。  

## ライブラリ開発のための最小限のフロー
### ライブラリの形をなすために
Pythonでは`setup.py`を書いて`root`ディレクトリに置いておけばライブラリになる。  
今回はこんな感じ。  
```python
from setuptools import find_packages, setup

deps = ["pandas", "pyarrow"]
gcp_deps = ["google-cloud-storage>=2.6.0", "google-cloud-bigquery", "pyarrow"]
test_requires = ["pytest"]

setup(
    name="dcraft",
    version="0.1.0",
    packages=find_packages(exclude=["tests"]),
    install_requires=deps,
    test_requires=test_requires,
    long_description=open("README.md").read(),
    long_description_content_type="text/markdown",
    extras_require={"gcp": gcp_deps, "test": test_requires},
    author="Shuhei Kishi",
)

```
正直この`setup.py`だと雑なので後で色々弄くる。  
### Pypi
Githubからコードをcloneしてローカルからインストールすることもできるけれど、Pypiにアップロードすると誰でも簡単に`pip install`コマンドだけでインストールができる。  
以下のリンクにアクセスしてユーザー登録。テスト環境もあるのでそっちも登録推奨。  
https://pypi.org/

### CI/CD
とりあえずテストとデプロイは自動化したい。JenkinsでもGithub Actionsでも良いから使うべき。  
個人的には、個人開発でGithubを使ってるならGithub Actionsが楽でおすすめ。  
プロジェクトの`root`ディレクトリに`.github/workflows`ディレクトリを作って、ymlファイルを置くだけで良し。  
今回はこんな感じ。ごちゃごちゃ書いてあるけど、テストして、ビルドしてデプロイしてるだけ。
```
name: Publish to PyPI
on: [push]
jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11"]

    steps:
      - uses: actions/checkout@v3
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v3
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e .[test]
      - name: Test with pytest
        run: |
          python -m pytest test
  build:
    name: Build distribution 📦
    runs-on: ubuntu-latest
    needs:
      - test
    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: "3.x"
    - name: Install pypa/build
      run: >-
        python3 -m
        pip install
        build
        --user
    - name: Build a binary wheel and a source tarball
      run: python3 -m build
    - name: Store the distribution packages
      uses: actions/upload-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
  publish-to-pypi:
    name: >-
      Publish Python 🐍 distribution 📦 to PyPI
    if: startsWith(github.ref, 'refs/tags/')
    needs:
    - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/dcraft
    permissions:
      id-token: write
    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1
```
### ドキュメンテーション
今回はSphinxを使用。以下のコマンドでテンプレートを作成して弄っていく。
```bash
pip install sphinx
sphinx-quickstart
```
docstringからの作成部分は以下のコマンドで。  
```bash
sphinx-apidoc -f -o ./docs/source/code_generated .
```
## 感想
仕事でもプライベートでも毎日そこそこの開発は行って来ているので気軽に始めたけれど、やってみると気にすることが多くて思っていたよりもしんどかったというのが正直なところ。  
せっかくなので継続的に開発を続けていきたい。  
## おまけ
Github上では使用してる言語の割合をグラフで出してるんだけど、このプロジェクトを始めるまでは全てのPythonプロジェクトをプライベートリポで進めてた。データサイエンティスト/機械学習エンジニアとして働いてるのに使用言語にPythonはない状態が続いていた。この開発でPythonが使用言語に出てきたのでGithub上の情報がデータサイエンティストっぽくなった。
![Screenshot from 2023-10-10 19-19-18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122985/a7289078-122a-4996-461e-d56b07df0f02.png)