paint-brush
Trello ボード管理用の Python CLI プログラムを作成する方法 (パート 2)@elainechan01
1,777 測定値
1,777 測定値

Trello ボード管理用の Python CLI プログラムを作成する方法 (パート 2)

Elaine Yun Ru Chan27m2023/11/07
Read on Terminal Reader

長すぎる; 読むには

Trello ボード管理用の Python CLI プログラムを作成する方法に関するチュートリアル シリーズのパート 2。CLI コマンドと Python パッケージ配布用のビジネス ロジックの作成方法に焦点を当てています。
featured image - Trello ボード管理用の Python CLI プログラムを作成する方法 (パート 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

もう定番のじゃんけん学校プロジェクトをはるかに超えています。早速、その内容に飛び込んでみましょう。


このチュートリアルを通じて何を達成できるのでしょうか?

Trello ボード管理用の Python CLI プログラムを作成する方法 (パート 1)では、Trello SDK と対話するビジネス ロジックを正常に作成しました。


ここでは、CLI プログラムのアーキテクチャを簡単に要約します。

要件に基づいた CLI 構造の詳細なテーブル ビュー


このチュートリアルでは、機能要件と非機能要件に焦点を当てて、プロジェクトを CLI プログラムに変換する方法を見ていきます。


一方で、プログラムをPyPI上のパッケージとして配布する方法も学びます。


始めましょう


フォルダー構造

以前は、 trelloserviceモジュールをホストするためのスケルトンをセットアップすることができました。今回は、さまざまな機能のモジュールを含むcliフォルダーを実装したいと思います。


  • 構成
  • アクセス
  • リスト


この考え方は、コマンド グループごとに、そのコマンドが独自のモジュールに保存されるということです。 listコマンドに関しては、どのコマンド グループにも属さないため、メイン CLI ファイルに保存します。


一方、フォルダー構造のクリーンアップを見てみましょう。より具体的には、ディレクトリが乱雑にならないようにすることで、ソフトウェアのスケーラビリティを考慮することから始める必要があります。


フォルダー構造に関する提案は次のとおりです。

 trellocli/ __init__.py __main__.py trelloservice.py shared/ models.py custom_exceptions.py cli/ cli.py cli_config.py cli_create.py tests/ test_cli.py test_trelloservice.py assets/ images/ README.md pyproject.toml .env .gitignore


assetsフォルダーがあることに気づきましたか?これはREADMEの関連アセットを保存するために使用されますが、 trellocliには新しく実装されたsharedフォルダーがあり、ソフトウェア全体で使用されるモジュールを保存するために使用します。


設定

まず、エントリ ポイント ファイル__main__.pyを変更しましょう。インポート自体を見ると、関連するモジュールを独自のサブフォルダーに保存することにしたため、そのような変更に対応する必要があります。一方で、メインの CLI モジュールcli.pyには、実行できるappインスタンスがあることも想定しています。

 # trellocli/__main__.py # module imports from trellocli import __app_name__ from trellocli.cli import cli from trellocli.trelloservice import TrelloService # dependencies imports # misc imports def main(): cli.app(prog_name=__app_name__) if __name__ == "__main__": main()


cli.pyファイルに早送りしてください。ここにappインスタンスを保存します。アイデアは、ソフトウェア全体で共有されるようにTyperオブジェクトを初期化することです。

 # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer()


この概念を進めて、 pyproject.tomlを変更してコマンド ライン スクリプトを指定しましょう。ここでは、パッケージの名前を指定し、エントリ ポイントを定義します。

 # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main"


上記のサンプルに基づいて、パッケージ名としてtrellocli定義し、 trellocliモジュールに保存されている__main__スクリプト内のmain関数が実行時に実行されます。


ソフトウェアの CLI 部分が設定されたので、CLI プログラムをより適切に提供できるようにtrelloserviceモジュールを変更しましょう。思い出したように、 trelloserviceモジュールは、承認されるまでユーザーの承認を再帰的に求めるように設定されています。許可が与えられない場合にプログラムが終了し、ユーザーにconfig accessコマンドの実行を促すようにこれを変更します。これにより、プログラムがより明確になり、命令に関してより説明的なものになります。


これを言葉で表すと、次の関数を変更します。


  • __init__
  • __load_oauth_token_env_var
  • authorize
  • is_authorized


__init__関数から始めて、ここでクライアントのセットアップを処理する代わりに、空のクライアントを初期化します。

 # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None


チャレンジ コーナー💡 ユーザーの承認を再帰的に求めるプロンプトが表示されないように、 __load_oauth_token_env_var関数を変更できますか?ヒント: 再帰関数とは、それ自体を呼び出す関数です。


authorizeおよびis_authorizedヘルパー関数に移ります。この概念は、 authorize __load_oauth_token_env_var関数を利用してクライアントをセットアップするビジネス ロジックを実行するのに対し、 is_authorized関数は単に認可が与えられたかどうかのブール値を返すだけであるということです。

 # trellocli/trelloservice.py class TrelloService: def authorize(self) -> AuthorizeResponse: """Method to authorize program to user's trello account Returns AuthorizeResponse: success / error """ self.__load_oauth_token_env_var() load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): return AuthorizeResponse(status_code=TRELLO_AUTHORIZATION_ERROR) else: self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) return AuthorizeResponse(status_code=SUCCESS) def is_authorized(self) -> bool: """Method to check authorization to user's trello account Returns bool: authorization to user's account """ if not self.__client: return False else: return True


__load_oauth_token_env_varauthorizeの違いは、 __load_oauth_token_env_varが環境変数として認可トークンを保存する内部向けの関数であるのに対し、 authorize公開向けの関数であり、必要な認証情報をすべて取得して Trello クライアントを初期化しようとすることです。


チャレンジ コーナー💡 authorize関数がAuthorizeResponseデータ型を返す方法に注目してください。 status_code属性を持つモデルを実装できますか? Trello ボード管理用の Python CLI プログラムの作成方法のパート 1を参照してください (ヒント: モデルの作成方法を見てください)。


最後に、モジュールの下部にシングルトンTrelloServiceオブジェクトをインスタンス化しましょう。完全なコードがどのように見えるかを確認するには、このパッチを参照してください: trello-cli-kit

 # trellocli/trelloservice.py trellojob = TrelloService()


最後に、プログラム全体で共有されるいくつかのカスタム例外を初期化します。これは、イニシャライザで定義されたERRORSとは異なります。これらの例外はBaseExceptionのサブクラスであり、一般的なユーザー定義の例外として機能しますが、 ERRORSは 0 から始まる定数値として機能します。


例外を最小限に抑え、最も注目に値するいくつかの一般的な使用例を見てみましょう。


  • 読み取りエラー: Trello からの読み取りエラーが発生した場合に発生します。
  • 書き込みエラー: Trello への書き込みエラーが発生した場合に発生します。
  • 認証エラー: Trello に対する認証が付与されていない場合に発生します。
  • 無効なユーザー入力エラー: ユーザーの CLI 入力が認識されない場合に発生します


# trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass


単体テスト

パート I で述べたように、このチュートリアルでは単体テストについては詳しく説明しません。そのため、必要な要素のみを扱います。


  • アクセスを構成するためのテスト
  • Trello ボードの構成をテストする
  • 新しい Trello カードを作成するテスト
  • Trello ボードの詳細を表示するテスト
  • trello ボードの詳細 (詳細ビュー) を表示するテスト


このアイデアは、期待される結果をテストするためのshellのようなコマンド ライン インタープリターを模擬することです。 Typerモジュールの優れている点は、独自のrunnerオブジェクトが付属していることです。テストの実行に関しては、 pytestモジュールと組み合わせます。詳細については、 Typer による公式ドキュメントを参照してください。


最初のテスト、つまりアクセスを構成するテストを一緒にやってみましょう。関数が適切に実行されるかどうかをテストしていることを理解してください。そのために、システムの応答と、終了コードがsuccess (別名 0) であるかどうかを確認します。終了コードとは何か、およびシステムがそれを使用してプロセスを通信する方法については、 RedHat による優れた記事を参照してください。

 # trellocli/tests/test_cli.py # module imports from trellocli.cli.cli import app # dependencies imports from typer.testing import CliRunner # misc imports runner = CliRunner() def test_config_access(): res = runner.invoke(app, ["config", "access"]) assert result.exit_code == 0 assert "Go to the following link in your browser:" in result.stdout


チャレンジ コーナー💡要点は理解できたので、他のテスト ケースを自分で実装できますか? (ヒント: 失敗ケースのテストも考慮する必要があります)


ビジネスの論理


メイン CLI モジュール

これがメインのcliモジュールになることを理解してください。すべてのコマンド グループ (config、create) のビジネス ロジックは、読みやすさを高めるために独自の別個のファイルに保存されます。


このモジュールでは、 listコマンドを保存します。コマンドをさらに深く掘り下げると、次のオプションを実装する必要があることがわかります。


  • board_name: config board事前に設定されていない場合は必須
  • 詳細: 詳細ビューで表示します。


これを実現するには、board_name 必須オプションをはじめ、いくつかの方法があります。そのうちの 1 つは、コールバック関数を使用する方法 (詳細については、公式ドキュメントを参照してください)、または単純にデフォルトの環境変数を使用する方法です。ただし、このユースケースでは、条件が満たされない場合にInvalidUserInputErrorカスタム例外を発生させることで、処理をわかりやすくしましょう。


コマンドを構築するには、オプションの定義から始めましょう。 Typer では、公式ドキュメントで述べられているように、オプションを定義するための重要な要素は次のとおりです。


  • データ・タイプ
  • ヘルパーテキスト
  • デフォルト値


たとえば、次の条件でdetailedオプションを作成します。


  • データ型: ブール値
  • ヘルパーテキスト: 「詳細ビューを有効にする」
  • デフォルト値: なし


コードは次のようになります。

 detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None


全体として、必要なオプションを指定してlistコマンドを定義するには、 list Python 関数として扱い、そのオプションを必須パラメーターとして扱います。

 # trellocli/cli/cli.py @app.command() def list( detailed: Annotated[bool, Option(help="Enable detailed view")] = None, board_name: Annotated[str, Option(help="Trello board to search")] = "" ) -> None: pass


ファイルの先頭に向かって初期化されたappインスタンスにコマンドを追加していることに注意してください。公式 Typer コードベースを自由に移動して、好みに合わせてオプションを変更してください。


コマンドのワークフローとしては、次のようなものを予定しています。


  • 権限の確認
  • 適切なボードを使用するように設定します ( board_nameオプションが指定されているかどうかを確認してください)
  • 読み取り対象となる Trello ボードをセットアップする
  • 適切な Trello カード データを取得し、Trello リストに基づいて分類します。
  • データ表示( detailedオプションが選択されているか確認)


注意すべき点がいくつかあります…


  • trellojob がSUCCESS以外のステータス コードを生成した場合に例外を発生させる必要があります。 try-catchブロックを使用すると、プログラムの致命的なクラッシュを防ぐことができます。
  • 使用する適切なボードを構成するときは、取得したboard_idに基づいて Trello ボードを使用できるように設定しようとします。したがって、次のユースケースをカバーしたいと考えています。
    • trellojob のget_all_boards関数を使用して一致をチェックすることにより、 board_name明示的に指定されている場合は、 board_idを取得します。
    • board_nameオプションが使用されなかった場合に、環境変数として保存されているboard_idを取得する
  • 表示されるデータは、 richパッケージのTable機能を使用してフォーマットされます。 richの詳細については、公式ドキュメントを参照してください。
    • 詳細: Trello リストの数、カードの数、定義されたラベルの概要を表示します。 Trello リストごとに、すべてのカードとその対応する名前、説明、および関連するラベルを表示します。
    • 非詳細: Trello リストの数、カードの数、定義されたラベルの概要を表示します。


すべてをまとめると、次のようになります。免責事項: TrelloServiceにはまだ実装されていない機能がいくつかある可能性があります。実装にヘルプが必要な場合は、このパッチを参照してください: trello-cli-kit

 # trellocli/cli/cli.py # module imports from trellocli.trelloservice import trellojob from trellocli.cli import cli_config, cli_create from trellocli.misc.custom_exceptions import * from trellocli import SUCCESS # dependencies imports from typer import Typer, Option from rich import print from rich.console import Console from rich.table import Table from dotenv import load_dotenv # misc imports from typing_extensions import Annotated import os # singleton instances app = Typer() console = Console() # init command groups app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") app.add_typer(cli_create.app, name="create", help="COMMAND GROUP to create new Trello elements") @app.command() def list( detailed: Annotated[ bool, Option(help="Enable detailed view") ] = None, board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to list board details in a simplified (default)/detailed view OPTIONS detailed (bool): request for detailed view board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError labels_list = res_get_all_labels.res res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError trellolists_list = res_get_all_lists.res # store data on cards for each trellolist trellolists_dict = {trellolist: [] for trellolist in trellolists_list} for trellolist in trellolists_list: res_get_all_cards = trellojob.get_all_cards(trellolist=trellolist) if res_get_all_cards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving cards from trellolist") raise TrelloReadError cards_list = res_get_all_cards.res trellolists_dict[trellolist] = cards_list # display data (lists count, cards count, labels) # if is_detailed OPTION is selected, display data (name, description, labels) for each card in each trellolist print() table = Table(title="Board: "+board.name, title_justify="left", show_header=False) table.add_row("[bold]Lists count[/]", str(len(trellolists_list))) table.add_row("[bold]Cards count[/]", str(sum([len(cards_list) for cards_list in trellolists_dict.values()]))) table.add_row("[bold]Labels[/]", ", ".join([label.name for label in labels_list if label.name])) console.print(table) if detailed: for trellolist, cards_list in trellolists_dict.items(): table = Table("Name", "Desc", "Labels", title="List: "+trellolist.name, title_justify="left") for card in cards_list: table.add_row(card.name, card.description, ", ".join([label.name for label in card.labels if label.name])) console.print(table) print() except (AuthorizationError, InvalidUserInputError, TrelloReadError): print("Program exited...")


ソフトウェアの動作を確認するには、ターミナルでpython -m trellocli --helpを実行するだけです。デフォルトでは、Typer モジュールは独自に--helpコマンドの出力を設定します。そして、パッケージ名としてtrellocli呼び出す方法に注目してください。これが以前にpyproject.tomlでどのように定義されていたかを思い出してください。


少し早送りして、 createコマンド グループとconfigコマンド グループも初期化しましょう。これを行うには、 appオブジェクトでadd_typer関数を使用するだけです。考え方としては、コマンド グループが独自のappオブジェクトを持ち、それをコマンド グループの名前とヘルパー テキストとともにcli.pyのメインappに追加するだけです。このように見えるはずです

# trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations")


チャレンジコーナー💡create createグループを自分でインポートできますか?ヘルプが必要な場合は、このパッチを参照してください: trello-cli-kit


サブコマンド

createのコマンド グループを設定するには、それぞれのコマンドを独自のモジュールに保存します。設定はcli.pyの設定と似ていますが、Typer オブジェクトをインスタンス化する必要があります。コマンドに関しては、カスタム例外を使用する必要性も遵守したいと考えています。さらに取り上げたいトピックは、ユーザーがCtrl + Cを押したとき、つまりプロセスを中断したときです。 listコマンドでこれを説明しなかった理由は、ここでの違いは、 configコマンド グループが対話型コマンドで構成されているためです。対話型コマンドの主な違いは、継続的なユーザー対話が必要であることです。もちろん、直接コマンドの実行には長い時間がかかるとします。潜在的なキーボード割り込みを処理することもベスト プラクティスです。


accessコマンドから始めて、最後にTrelloServiceで作成したauthorize関数を使用します。 authorize関数は構成をすべて独自に処理するため、プロセスの実行を確認するだけで済みます。

 # trellocli/cli/cli_config.py @app.command() def access() -> None: """COMMAND to configure authorization for program to access user's Trello account""" try: # check authorization res_authorize = trellojob.authorize() if res_authorize.status_code != SUCCESS: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except AuthorizationError: print("Program exited...")


boardコマンドに関しては、ユーザー対話用のターミナル GUI を表示するシンプル ターミナル メニューなど、優れたユーザー エクスペリエンスを提供するためにさまざまなモジュールを利用します。主なアイデアは次のとおりです。


  • 認可を確認する
  • ユーザーのアカウントからすべての Trello ボードを取得します
  • Trello ボードの単一選択ターミナル メニューを表示する
  • 選択した Trello ボード ID を環境変数として設定します


# trellocli/cli/cli_config.py @app.command() def board() -> None: """COMMAND to initialize Trello board""" try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # retrieve all boards res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving trello boards") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # for easy access to board id when given board name # display menu to select board boards_menu = TerminalMenu( boards_list.keys(), title="Select board:", raise_error_on_interrupt=True ) boards_menu.show() selected_board = boards_menu.chosen_menu_entry # set board ID as env var dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_BOARD_ID", value_to_set=boards_list[selected_board] ) except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, TrelloReadError): print("Program exited...")


最後に、ソフトウェアの中核となる機能要件、つまり Trello ボードのリストに新しいカードを追加する作業に移ります。 listコマンドからボードからデータを取得するまでは同じ手順を使用します。


それとは別に、新しいカードを適切に構成するために対話的にユーザー入力を要求します。


  • 追加する Trello リスト: 単一選択
  • カード名:テキスト
  • [オプション] カードの説明: テキスト
  • [オプション] ラベル: 複数選択
  • 確認: はい/いいえ


ユーザーがリストから選択する必要があるすべてのプロンプトについては、以前と同様にSimple Terminal Menuパッケージを使用します。他のプロンプトや、テキスト入力やユーザーの確認の必要性などのその他の項目については、単純にrichパッケージを使用します。オプションの値を適切に処理する必要があることに注意することも重要です。


  • ユーザーは説明の入力をスキップできます
  • ユーザーはラベルに空の選択肢を提供できます


# trellocli/cli/cli_create.py @app.command() def card( board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to add a new trello card OPTIONS board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the labels from the trello board") raise TrelloReadError labels_list = res_get_all_labels.res labels_dict = {label.name: label for label in labels_list if label.name} res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the lists from the trello board") raise TrelloReadError trellolists_list = res_get_all_lists.res trellolists_dict = {trellolist.name: trellolist for trellolist in trellolists_list} # for easy access to trellolist when given name of trellolist # request for user input (trellolist, card name, description, labels to include) interactively to configure new card to be added trellolist_menu = TerminalMenu( trellolists_dict.keys(), title="Select list:", raise_error_on_interrupt=True ) # Prompt: trellolist trellolist_menu.show() print(trellolist_menu.chosen_menu_entry) selected_trellolist = trellolists_dict[trellolist_menu.chosen_menu_entry] selected_name = Prompt.ask("Card name") # Prompt: card name selected_desc = Prompt.ask("Description (Optional)", default=None) # Prompt (Optional) description labels_menu = TerminalMenu( labels_dict.keys(), title="Select labels (Optional):", multi_select=True, multi_select_empty_ok=True, multi_select_select_on_accept=False, show_multi_select_hint=True, raise_error_on_interrupt=True ) # Prompt (Optional): labels labels_menu.show() selected_labels = [labels_dict[label] for label in list(labels_menu.chosen_menu_entries)] if labels_menu.chosen_menu_entries else None # display user selection and request confirmation print() confirmation_table = Table(title="Card to be Added", show_header=False) confirmation_table.add_row("List", selected_trellolist.name) confirmation_table.add_row("Name", selected_name) confirmation_table.add_row("Description", selected_desc) confirmation_table.add_row("Labels", ", ".join([label.name for label in selected_labels]) if selected_labels else None) console.print(confirmation_table) confirm = Confirm.ask("Confirm") # if confirm, attempt to add card to trello # else, exit if confirm: res_add_card = trellojob.add_card( col=selected_trellolist, name=selected_name, desc=selected_desc, labels=selected_labels ) if res_add_card.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when adding a new card to trello") raise TrelloWriteError else: print("Process cancelled...") except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, InvalidUserInputError, TrelloReadError, TrelloWriteError): print("Program exited...")


チャレンジコーナー💡 addプロセスの進行状況バーを表示できますか?ヒント: richステータス機能の使用方法を見てください。


パッケージの配布

ここからが楽しい部分です。PyPI でソフトウェアを正式に配布します。これを行うには、次のパイプラインに従います。


  • メタデータの構成 + README の更新
  • テスト PyPI にアップロードする
  • GitHub アクションの構成
  • コードをタグ v1.0.0 にプッシュします
  • コードを PyPI に配布する 🎉


詳細な説明については、 Ramit Mittal による Python パッケージングに関するこの素晴らしいチュートリアルを参照してください。


メタデータの構成

pyproject.tomlに必要な最後の詳細は、パッケージ自体を格納するモジュールを指定することです。私たちの場合、それはtrellocliになります。追加するメタデータは次のとおりです。

 # pyproject.toml [tool.setuptools] packages = ["trellocli"]


README.mdに関しては、使用ガイドラインや開始方法など、何らかのガイドを提供することをお勧めします。 README.mdに画像を含めた場合は、その絶対 URL を使用する必要があります。通常は次の形式です。

 https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image>


テストPyPI

buildツールとtwineツールを使用して、パッケージをビルドして公開します。ターミナルで次のコマンドを実行して、パッケージのソース アーカイブとホイールを作成します。

 python -m build


TestPyPI でアカウントがすでにセットアップされていることを確認し、次のコマンドを実行します。

 twine upload -r testpypi dist/*


ユーザー名とパスワードを入力するように求められます。 2 要素認証が有効になっているため、API トークンを使用する必要があります (TestPyPI API トークンの取得方法の詳細については、ドキュメントへのリンクを参照してください)。次の値を入力するだけです。


  • ユーザー名:トークン
  • パスワード: <あなたのTestPyPIトークン>


それが完了したら、TestPyPI に移動して、新しく配布されたパッケージをチェックアウトできるようになります。


GitHub のセットアップ

目標は、タグに基づいてパッケージの新しいバージョンを継続的に更新する手段として GitHub を利用することです。


まず、GitHub ワークフローのActionsタブに移動し、新しいワークフローを選択します。 GitHub Actions によって作成されたPublish Python Packageワークフローを使用します。ワークフローでリポジトリ シークレットからの読み取りがどのように必要であるかに注目してください。 PyPI トークンが指定された名前で保存されていることを確認してください (PyPI API トークンの取得は TestPyPI の取得と似ています)。


ワークフローが作成されたら、コードをタグ v1.0.0 にプッシュします。バージョン命名構文の詳細については、Py-Pkgs による優れた説明を参照してください:ドキュメントへのリンク


通常のpulladdcommitコマンドを実行するだけです。次に、次のコマンドを実行して、最新のコミットのタグを作成します (タグの詳細については、ドキュメントへのリンクを参照してください)。

 git tag <tagname> HEAD


最後に、新しいタグをリモート リポジトリにプッシュします。

 git push <remote name> <tag name>


さらに詳しく知りたい場合は、Karol Horosin による CI/CD と Python パッケージの統合に関する素晴らしい記事を参照してください。しかし今は、落ち着いて最新の成果をお楽しみください 🎉。パッケージを PyPI に配布するときに、GitHub Actions ワークフローとして魔法が解き明かされるのを自由に観察してください。


まとめ

長くなりました😓。このチュートリアルを通じて、 Typerモジュールを使用してソフトウェアを CLI プログラムに変換し、パッケージを PyPI に配布する方法を学びました。さらに深く理解するために、コマンドとコマンド グループを定義し、対話型 CLI セッションを開発し、キーボード割り込みなどの一般的な CLI シナリオに取り組む方法を学習しました。


あなたはすべてを乗り越えた絶対的な魔法使いでした。オプション機能を実装するパート 3 に参加しませんか?