📝

ChatGPT(gpt4o)でソフトウェア治具をさっと作る例2

2024/06/23に公開

ChatGPT(gpt4o)でソフトウェア治具をさっと作る例2

はじめに

ChatGPT (GPT-4) を使って簡単なツール(治具)を作成し、PyPIに公開するまでの過程を紹介します。

今回は、AWS DynamoDBの操作を支援するコマンドラインツールを作りました。
このツールはDynamoDBテーブルのリスト表示とデータエクスポート機能を持っています。

ステップ1: ツールの仕様決定とコーディング

まず、作成するツールの仕様を決定します。今回作成するツールの内容を結構細かく考えつつ、プロンプトを書きます。

指示:

pythonのコマンドラインツールの実装を手伝ってください。
- clickパッケージを使います。
- コマンド名は、ddb_exporterコマンドとします。aws dynamo-db操作用のユーティリティツールです。
  - 任意パラメータとして aws-profileを取り、指定されたプロファイル名でawsアクセスするものとします(指定がなければAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN環境変数を使用。これらもなければデフォルトプロファイルを使用。)
  - 認証で失敗した場合、どうすればよいか、エラーメッセージに丁寧なガイドを含める。
- サブコマンドにlist-tables, export-tableがあります。
- list-tablesサブコマンドでは、--regionで指定された(またはデフォルトの)リージョンにある、テーブル名の一覧を出力します。
  - `--detail` オプションが有効な場合は、スキーマ情報も出力します。
- export-tableサブコマンドでは、--region, --table で指定されたリージョン、テーブル名のデータを出力します。
  - 引数としてjsonファイルパスを取ります(指定されなかった場合はテーブル名+タイムスタンプから生成)。ファイルがすでに存在する場合は上書きするかどうかの確認をするものとします。ただし--overwriteオプションが指定された場合は確認なしで上書きします。
  - `--partition-key`, `--sort-key`パラメタ(任意)で絞り込むことが可能です。ソートキーでは範囲条件を設定可能とします。また、セカンダリインデックスによる抽出条件設定も可能とします(このあたりのパラメータ設計はお任せします)。

今回はAWSアクセスキーという毎回入力するのはしんどい項目があったので、環境変数を使うことにしましたが、変数名を明示することで、ChatGPTでコードを再生成したときに変数名が変わらないようにしています。

また、初回に出力されたコードではAPIアクセス時のデータ出力件数制限が考慮されていなかったので、以下のようにやんわり指摘してみたところ、全件が出力されるようにコードを修正してくれました。

このコマンドのexport-table の出力件数に制限はありますか?

出力:

import click
import boto3
from botocore.exceptions import NoCredentialsError, PartialCredentialsError, ClientError
import json
import os
from datetime import datetime

def get_dynamodb_client(profile, region):
    session = boto3.Session(profile_name=profile) if profile else boto3.Session()
    return session.client('dynamodb', region_name=region)

@click.group()
@click.option('--aws-profile', default=None, help='AWS profile name to use for accessing DynamoDB.')
@click.pass_context
def ddb_exporter(ctx, aws_profile):
    """DynamoDB operations utility tool."""
    ctx.ensure_object(dict)
    ctx.obj['AWS_PROFILE'] = aws_profile

@ddb_exporter.command()
@click.option('--region', default='us-east-1', help='AWS region to use.')
@click.option('--detail', is_flag=True, help='Include schema information of tables.')
@click.pass_context
def list_tables(ctx, region, detail):
    """List DynamoDB tables in the specified region."""
    try:
        client = get_dynamodb_client(ctx.obj['AWS_PROFILE'], region)
        response = client.list_tables()
        tables = response.get('TableNames', [])
        if detail:
            for table in tables:
                table_info = client.describe_table(TableName=table)
                click.echo(json.dumps(table_info, indent=2))
        else:
            click.echo(json.dumps(tables, indent=2))
    except (NoCredentialsError, PartialCredentialsError):
        click.echo("AWS credentials not found. Please configure your credentials or specify a profile.")
    except ClientError as e:
        click.echo(f"Failed to list tables: {str(e)}")

@ddb_exporter.command()
@click.option('--region', default='us-east-1', help='AWS region to use.')
@click.option('--table', required=True, help='DynamoDB table name to export.')
@click.option('--output', default=None, help='Output JSON file path.')
@click.option('--overwrite', is_flag=True, help='Overwrite output file if it exists.')
@click.option('--partition-key', default=None, help='Partition key to filter the data.')
@click.option('--sort-key', default=None, help='Sort key to filter the data.')
@click.option('--sort-key-range', default=None, help='Sort key range to filter the data (format: start,end).')
@click.option('--index-name', default=None, help='Secondary index name to query.')
@click.option('--index-partition-key', default=None, help='Partition key for the secondary index.')
@click.option('--index-sort-key', default=None, help='Sort key for the secondary index.')
@click.pass_context
def export_table(ctx, region, table, output, overwrite, partition_key, sort_key, sort_key_range, index_name, index_partition_key, index_sort_key):
    """Export data from DynamoDB table."""
    try:
        client = get_dynamodb_client(ctx.obj['AWS_PROFILE'], region)
        
        if not output:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            output = f"{table}_{timestamp}.json"
        
        if os.path.exists(output) and not overwrite:
            click.confirm(f"{output} already exists. Do you want to overwrite?", abort=True)
        
        scan_kwargs = {}
        
        if partition_key:
            scan_kwargs['FilterExpression'] = f"partition_key = :pk"
            scan_kwargs['ExpressionAttributeValues'] = {":pk": {"S": partition_key}}
        
        if sort_key:
            if sort_key_range:
                start, end = sort_key_range.split(',')
                scan_kwargs['FilterExpression'] += f" AND sort_key BETWEEN :start AND :end"
                scan_kwargs['ExpressionAttributeValues'].update({":start": {"S": start}, ":end": {"S": end}})
            else:
                scan_kwargs['FilterExpression'] += f" AND sort_key = :sk"
                scan_kwargs['ExpressionAttributeValues'].update({":sk": {"S": sort_key}})
        
        if index_name:
            scan_kwargs['IndexName'] = index_name
            if index_partition_key:
                scan_kwargs['FilterExpression'] = f"index_partition_key = :ipk"
                scan_kwargs['ExpressionAttributeValues'] = {":ipk": {"S": index_partition_key}}
            if index_sort_key:
                scan_kwargs['FilterExpression'] += f" AND index_sort_key = :isk"
                scan_kwargs['ExpressionAttributeValues'].update({":isk": {"S": index_sort_key}})
        
        paginator = client.get_paginator('scan')
        items = []
        for page in paginator.paginate(TableName=table, **scan_kwargs):
            items.extend(page.get('Items', []))
        
        with open(output, 'w') as f:
            json.dump(items, f, indent=2)
        
        click.echo(f"Data exported to {output}")
    
    except (NoCredentialsError, PartialCredentialsError):
        click.echo("AWS credentials not found. Please configure your credentials or specify a profile.")
    except ClientError as e:
        click.echo(f"Failed to export table: {str(e)}")

if __name__ == '__main__':
    ddb_exporter()

ステップ2: README.md の記述

別のところでさっと使えたら良いような気がしたので、PyPIに公開することを考えて、README.mdファイルを作成しました。
(後になって、これもpyinstallerのほうが良かったなと思い直しましたが、例ということでそのまま続けます)

指示:

README.mdを書いてください。

出力:

# ddb_exporter

`ddb_exporter` は、AWS DynamoDBのテーブル操作およびデータエクスポートを簡単に行うためのコマンドラインツールです。

## インストール

`ddb_exporter` をインストールするには、以下のコマンドを実行してください:

```bash
pip install click boto3
```

次に、スクリプトファイルを実行可能にして、適切なディレクトリに配置します。

## 使い方

`ddb_exporter` コマンドは、AWSのDynamoDBサービスにアクセスして操作を行うためのサブコマンドを提供します。以下に各サブコマンドの詳細を示します。

### 共通オプション

- `--aws-profile` (オプション): 使用するAWSプロファイル名。指定がない場合、デフォルトプロファイルまたは環境変数で設定された認証情報が使用されます。

### サブコマンド

#### `list-tables`

指定したリージョンのDynamoDBテーブルを一覧表示します。

```bash
ddb_exporter list-tables --region <region> [--detail]
```

- `--region` (オプション): AWSリージョン。デフォルトは `us-east-1` です。
- `--detail` (オプション): テーブルのスキーマ情報も含めて表示します。

#### `export-table`

指定したDynamoDBテーブルのデータをJSONファイルにエクスポートします。

```bash
ddb_exporter export-table --region <region> --table <table_name> [--output <output_file>] [--overwrite] [--partition-key <key>] [--sort-key <key>] [--sort-key-range <start,end>] [--index-name <index_name>] [--index-partition-key <key>] [--index-sort-key <key>]
```

- `--region` (オプション): AWSリージョン。デフォルトは `us-east-1` です。
- `--table` (必須): エクスポートするDynamoDBテーブル名。
- `--output` (オプション): 出力先のJSONファイルパス。指定がない場合は `テーブル名_タイムスタンプ.json` 形式で生成されます。
- `--overwrite` (オプション): 出力ファイルが既に存在する場合に上書きします。
- `--partition-key` (オプション): パーティションキーでデータをフィルタリングします。
- `--sort-key` (オプション): ソートキーでデータをフィルタリングします。
- `--sort-key-range` (オプション): ソートキーの範囲条件でデータをフィルタリングします(フォーマット: `start,end`)。
- `--index-name` (オプション): セカンダリインデックス名。
- `--index-partition-key` (オプション): セカンダリインデックスのパーティションキーでデータをフィルタリングします。
- `--index-sort-key` (オプション): セカンダリインデックスのソートキーでデータをフィルタリングします。

## 認証情報の設定

AWSの認証情報は、以下の方法で設定できます:

1. AWS CLIでプロファイルを設定する。
    ```bash
    aws configure --profile <profile_name>
    ```
2. 環境変数を設定する。
    ```bash
    export AWS_ACCESS_KEY_ID=<your_access_key_id>
    export AWS_SECRET_ACCESS_KEY=<your_secret_access_key>
    export AWS_SESSION_TOKEN=<your_session_token>  # 必要な場合
    ```

## エラー処理

認証情報が見つからない場合や認証に失敗した場合、以下のエラーメッセージが表示されます:

```
AWS credentials not found. Please configure your credentials or specify a profile.
```

また、DynamoDB操作に失敗した場合、適切なエラーメッセージが表示されます。

## ライセンス

このプロジェクトはMITライセンスの下で公開されています。詳細については `LICENSE` ファイルを参照してください。

ステップ3: PyPIへの公開のための準備

やり方を忘れるので、毎回ききます。

指示:

ありがとう。
これをpypiに公開したいです。pypiのアカウントは取得済みで、キーも保存済みです。プロジェクトはpoetryで管理しています。どうすればよいですか?

なんかキーとか作って設定したよなー、というおぼろげな記憶を頼りに、そのへんの説明は不要だよ、と含みおきました。

出力:

長くなるので、実際の出力は割愛して、出力された手順に従って以下のようにしました。

PyPIにプロジェクトを公開するための手順を以下に示します。ここでは、poetry を使用してプロジェクトを管理し、PyPIに公開する方法を説明します。

  1. pyproject.toml の設定:
    pyproject.toml ファイルを設定します。このファイルには、プロジェクトのメタデータと依存関係が記述されています。

    [tool.poetry]
    name = "ddb_exporter"
    version = "0.1.0"
    description = "A command-line tool for AWS DynamoDB operations and data export."
    authors = ["Your Name <you@example.com>"]
    license = "MIT"
    
    [tool.poetry.dependencies]
    python = "^3.7"
    click = "^8.0"
    boto3 = "^1.17"
    
    [tool.poetry.scripts]
    ddb_exporter = "ddb_exporter:ddb_exporter"
    
    [build-system]
    requires = ["poetry-core>=1.0.0"]
    build-backend = "poetry.core.masonry.api"
    
  2. 必要なファイルを準備:
    以下のファイルをプロジェクトのルートディレクトリに用意します。

    • README.md
    • LICENSE(MITライセンスの場合、適切なライセンスファイルを用意します)
    • ddb_exporter.py(スクリプト本体)
  3. ビルドと公開:

    Poetryを使用してパッケージをビルドし、PyPIに公開する手順は以下の通りです。

    1. パッケージのビルド:

      poetry build
      

      このコマンドにより、dist/ ディレクトリに .tar.gz および .whl ファイルが生成されます。

    2. PyPIへの公開:

      poetry publish --username <your-username> --password <your-password>
      

      ここで、<your-username><your-password> はPyPIのアカウント情報です。PyPIのAPIトークンを使用する場合は、以下のようにします:

      poetry config pypi-token.pypi <your-token>
      poetry publish
      

まとめ

以上が、ChatGPT (GPT-4) を使用して簡単なDynamoDBユーティリティツールを作成し、PyPIに公開するまでの手順です。

手戻りを減らすために、以下のような順序が良いな、と思いました。

  1. 仕様をきっちり決める。
    1. 外部インタフェース(今回は環境変数)も決めておくと「テスト&生成しなおし」に対応しやすい
    2. 通常つくりながら増やしていくだろう細かいオプションも最初に書ききってしまう。
    3. 考えるの面倒なところは方針だけ決めて残りを明示的に「おまかせ」する。
  2. ドキュメントを生成させる
    1. コードとドキュメントの生成の順序は、コードとドキュメントとどっちをレビューするのが楽か、によると思う。
    2. 楽な方を先に生成させてしっかりレビューした後で、もう一方を生成したほうが楽な気がする(コードからドキュメント、ドキュメントからコード、はどちらも良い精度で生成できるのでこれのレビューはあまり頑張らなくて良いから)
  3. 公開する。

Discussion