今回は、生成AIに本連載で作ってきたPythonスクリプトを整える作業を行ってもらう。従来、こうした作業はプログラマーが自分で行うことが当然だったが、生成AIが登場してからは生成AIに行わせることができる。生産的ではないが時間を浪費する作業には、積極的に生成AIを活用して行きたいところだ。

連載「生成AI×プログラミング」のこれまでの回はこちらを参照

これまでの成果物

これまでの成果物をまとめておこう。SQLiteのデータ構造、構成しているPythonスクリプトファイル、アプリのサブコマンド、Pythonスクリプトを実行するためのPowerShellラッパースクリプトは次のようになる。

列名 内容
id INTEGER (主キー) タスクの一意識別子
title TEXT タスクのタイトル
description TEXT タスクの詳細な説明
due_date DATE タスクの期日
priority INTEGER タスクの優先度。1が最も高い優先度で、3が最も低い優先度
completed INTEGER タスクが完了したかどうかを示すフラグ。0は未完了、1は完了
ファイル名 内容
todo_app.py CUIの処理を行うファイル。ユーザーが直接操作する
todo_db_manager.py SQLiteデータベースとのやり取りを担当するファイル
todo_app.ps1 todo_app.pyを実行するためのラッパースクリプト
サブコマンド 引数 内容
add タスクの追加
remove id タスクの削除
list タスクの一覧表示

todo_app.py

import sys
import datetime
from todo_db_manager import create_todo_table, add_todo, remove_todo, get_todos

def parse_date(date_str):
    """日付文字列を datetime オブジェクトに変換 """
    for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y%m%d"):
        try:
            # 時刻が含まれている場合に備えて、日付部分のみを抽出
            date_time = datetime.datetime.strptime(date_str, fmt)
            return date_time.date()
        except ValueError:
            continue
    raise ValueError("無効な日付形式です。")

def main():    
    # コマンドライン引数の処理
    if len(sys.argv) < 2:
        print("サブコマンドが指定されていません。")
        return

    command = sys.argv[1]

    # テーブルの作成
    create_todo_table()

    if command == "add":
        # タスクの追加
        title = input("タイトルを入力してください (必須): ").strip()
        if not title:
            print("タイトルは必須です。")
            return

        description = input("説明を入力してください (任意): ").strip()

        # 期限の入力と処理
        date_input = input("期限を入力してください (年-月-日 または 年/月/日 または 年月日) (省略すると今日の日付になります): ").strip()
        if date_input:
            try:
                due_date = parse_date(date_input)
            except ValueError as e:
                print(e)
                return
        else:
            due_date = datetime.date.today()

        # プライオリティの入力と処理
        priority_input = input("プライオリティを入力してください (1, 2, 3) (省略すると3になります): ").strip()
        try:
            priority = int(priority_input) if priority_input else 3
            if priority not in {1, 2, 3}:
                raise ValueError("プライオリティは1, 2, 3のいずれかでなければなりません。")
        except ValueError as e:
            print(e)
            return

        # タスクの追加
        add_todo(title, description, due_date=due_date, priority=priority)
        print(f"タスク '{title}' が追加されました。")

    elif command == "remove":
        # ユーザーに数字の入力を求める
        try:
            todo_id = int(input("削除するタスクのIDを入力してください: "))
            remove_todo(todo_id)  # remove_todo 関数を呼び出す
            print(f"タスクID {todo_id} が削除されました。")
        except ValueError:
            print("無効なIDが入力されました。数字を入力してください。")
        except Exception as e:
            print(f"エラーが発生しました: {e}")

    elif command == "list":
        # すべてのタスクを一覧表示
        todos = get_todos()

        today = datetime.date.today()

        # タスクの期日列を日付形式で処理
        def get_due_date(todo):
            due_date_str = todo[3]
            if due_date_str:
                try:
                    # 日付部分だけを抽出するために、datetimeオブジェクトを使用する
                    return datetime.datetime.strptime(due_date_str.split()[0], "%Y-%m-%d").date()
                except ValueError:
                    return None
            return None

        # 期限が今日以降のタスクを取り出す
        upcoming_todos = [todo for todo in todos if get_due_date(todo) and get_due_date(todo) >= today]
        overdue_todos = [todo for todo in todos if get_due_date(todo) and get_due_date(todo) < today]
        no_due_date_todos = [todo for todo in todos if get_due_date(todo) is None]

        # 期限が今日以降のタスクを期日で昇順に並べる
        upcoming_todos.sort(key=lambda x: get_due_date(x))

        # 期限が今日より前のタスクを期日で降順に並べる
        overdue_todos.sort(key=lambda x: get_due_date(x), reverse=True)

        # 期限がないタスクは追加のソートなし

        # タスクの表示
        for todo in upcoming_todos:
            print(todo)

        print("----")

        for todo in overdue_todos:
            print(todo)

        for todo in no_due_date_todos:
            print(todo)

    else:
        print("無効なサブコマンドです。")

if __name__ == "__main__":
    main(

todo_db_manager.py

import sqlite3
from datetime import datetime

DB_FILE = "todos.db"

def create_todo_table():
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY,
            title TEXT,
            description TEXT,
            due_date DATE,
            priority INTEGER,
            completed INTEGER
        )
    ''')
    conn.commit()
    conn.close()

def add_todo(title, description=None, due_date=None, priority=3):
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute('''
        INSERT INTO todos (title, description, due_date, priority, completed)
        VALUES (?, ?, ?, ?, ?)
    ''', (title, description, due_date, priority, 0))
    conn.commit()
    conn.close()

def remove_todo(id):
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute('''
        DELETE FROM todos WHERE id = ?
    ''', (id,))
    conn.commit()
    conn.close()

def get_todos():
    conn = sqlite3.connect(DB_FILE)
    cursor = conn.cursor()
    cursor.execute('''
        SELECT * FROM todos
    ''')
    todos = cursor.fetchall()
    conn.close()
    return todo

todo_app.ps1

# PythonのパスとPythonスクリプトのパスを設定する
$pythonExe = "C:\Users\daichi\AppData\Local\Microsoft\WindowsApps\python.exe"
$pythonScript = "C:\Users\daichi\Documents\python\todo\todo_app.py"

# 現在のカレントディレクトリを保存する
$originalLocation = Get-Location

# Pythonスクリプトのディレクトリを取得する
$scriptDir = Split-Path $pythonScript -Parent

try {
    # Pythonスクリプトを実行する
    Push-Location $scriptDir
    & $pythonExe $pythonScript @args
}
catch {
    # エラーメッセージを表示する(必要に応じて)
    Write-Error "${pythonScript}実行中にエラーが発生しました: $_"
}
finally {
    # カレントディレクトリを戻す
    Pop-Location