免責事項: このチュートリアルは、読者が Python、API、Git、単体テストの基礎知識を持っていることを前提としています。 最高にクールなアニメーションを備えたさまざまな CLI ソフトウェアに出会ったので、「ミニマルな」じゃんけん学校プロジェクトをアップグレードできるだろうか、と疑問に思いました。 こんにちは、遊びましょう!ファイターを選択してください (ジャンケン): ロック CLI プログラムとは何ですか? Wikipedia に記載されているように、「コマンドライン インターフェイス (CLI) は、ユーザーまたはクライアントからのコマンドと、テキスト行の形式でのデバイスまたはプログラムからの応答によって、デバイスまたはコンピューター プログラムと対話する手段です。」 言い換えれば、CLI プログラムは、ユーザーがコマンド ラインを使用して、実行命令を提供することでプログラムと対話するプログラムです。 日常的なソフトウェアの多くは、CLI プログラムとしてラップされています。たとえば、 テキスト エディターを考えてみましょう。このツールは、UNIX システムに付属しており、ターミナルで 実行するだけでアクティブ化できます。 vim vim <FILE> に関して、CLI プログラムの構造を詳しく見てみましょう。 Google Cloud CLI 引数 引数(パラメータ)とは、プログラムに与える情報のことです。位置によって識別されるため、位置引数と呼ばれることがよくあります。 たとえば、コア セクションで プロパティを設定する場合は、 を実行します。 project gcloud config set project <PROJECT_ID> 特に、これを次のように翻訳できます。 口論 コンテンツ 引数0 gクラウド 引数1 構成 … … コマンド コマンドは、コンピューターに指示を与える引数の配列です。 前の例に基づいて、 を実行して、コア セクションに プロパティを設定します。 gcloud config set project <PROJECT_ID> project つまり、 はコマンドです。 set オプションのコマンド 通常、コマンドは必須ですが、例外を設けることもできます。プログラムのユースケースに基づいて、オプションのコマンドを定義できます。 コマンドに戻ると、公式ドキュメントに記載されているように、 プロパティを変更できるコマンド グループです。使用方法は次のとおりです。 gcloud config gcloud config gcloud config GROUP | COMMAND [GCLOUD_WIDE_FLAG … ] ここで、 COMMAND は 、 などになります… ( GROUP は であることに注意してください) set list config オプション オプションは、コマンドの動作を変更するパラメータの文書化されたタイプです。これらは、「-」または「--」で示されるキーと値のペアです。 コマンド グループの使用法に戻ると、この場合のオプションは です。 gcloud config GCLOUD_WIDE_FLAG たとえば、コマンドの詳細な使用法と説明を表示したい場合は、 を実行します。つまり、 がオプションです。 gcloud config set –help --help もう 1 つの例は、特定のプロジェクトのコンピューティング セクションでゾーン プロパティを設定する場合、 を実行することです。つまり、 値 を保持するオプションです。 gcloud config set compute <ZONE_NAME> –project=<PROJECT_ID> --project <PROJECT_ID> また、彼らの立場は通常は重要ではないことに注意することも重要です。 必須オプション オプションは、その名前と同様、通常はオプションですが、必須になるように調整することもできます。 たとえば、dataproc クラスタを作成する場合は、 を実行します。そして、使用方法のドキュメントに記載されているように: gcloud dataproc clusters create <CLUSTER_NAME> –region=<REGION> gcloud dataproc clusters create (CLUSTER: –region=REGION) フラグは、事前に構成されていない場合は必須です。 --region ショートオプションとロングオプション 短いオプションは で始まり、その後に 1 つの英数字が続きます。一方、長いオプションは で始まり、その後に複数の文字が続きます。短いオプションは、ユーザーが何を望んでいるのかがわかっている場合のショートカットとして考えてください。一方、長いオプションは読みやすいものです。 - -- ロックを選んだんですね!コンピューターが選択を行います。 このチュートリアルを通じて何を達成できるのでしょうか? それで、私は嘘をつきました…定番のじゃんけん CLI プログラムをアップグレードするつもりはありません。 代わりに、実際のシナリオを見てみましょう。 概要と目標 あなたのチームは Trello を使用してプロジェクトの問題と進捗状況を追跡しています。あなたのチームは、ターミナルを介して新しい GitHub リポジトリを作成するのと同じような、ボードと対話するためのより簡素化された方法を探しています。チームは、ボードの「To Do」列に新しいカードを追加できるという基本要件を備えた CLI プログラムの作成をあなたに依頼しました。 前述の要件に基づいて、要件を定義して CLI プログラムの草案を作成しましょう。 機能要件 ユーザーはボード上の列に新しいカードを追加できます 必須入力: 列、カード名 オプションの入力: カードの説明、カードのラベル (既存のものから選択) 非機能要件 Trello アカウントへのアクセス (認証) をユーザーに求めるプログラム どの Trello ボードで作業するかをユーザーに設定するよう求めるプログラム (構成) オプションの要件 ユーザーはボードに新しい列を追加できます ユーザーはボードに新しいラベルを追加できます ユーザーはすべての列の簡略化/詳細ビューを確認できます 上記に基づいて、CLI プログラムのコマンドとオプションを次のように形式化できます。 Ps 最後の 2 つの列については心配しないでください。それについては後ほど説明します… 私たちの技術スタックに関しては、これに固執します。 単体テスト pytest pytest-mock cli-テストヘルパー トレロ py-trello (Trello SDK の Python ラッパー) CLI タイパー リッチ 簡易メニュー ユーティリティ (その他) Python-dotenv タイムライン このプロジェクトは部分的に取り組んでいきます。期待できることの一部を以下に示します。 パート1 ビジネスロジックの実装 py-trello パート2 CLIビジネスロジックの実装 CLI プログラムをパッケージとして配布する パート 3 オプションの機能要件の実装 パッケージのアップデート コンピューターがハサミを選んだのです!誰がこの戦いに勝つか見てみましょう… 始めましょう フォルダー構造 目標は、CLI プログラムを 上のパッケージとして配布することです。したがって、次のような設定が必要です。 PyPI trellocli/ __init__.py __main__.py models.py cli.py trelloservice.py tests/ test_cli.py test_trelloservice.py README.md pyproject.toml .env .gitignore ここでは、各ファイルおよび/またはディレクトリについて詳しく説明します。 : ユーザーが使用するパッケージ名として機能します (例: trellocli pip install trellocli : パッケージのルートを表し、フォルダーを Python パッケージとして準拠させます __init__.py : エントリ ポイントを定義し、ユーザーが フラグを使用してファイル パスを指定せずにモジュールを実行できるようにします。たとえば、 で を置き換えます。 __main__.py -m python -m <module_name> python -m <parent_folder>/<module_name>.py : グローバルに使用されるクラス (API 応答が準拠すると予想されるモデルなど) を格納します。 models.py : CLI コマンドとオプションのビジネス ロジックを保存します。 cli.py : と対話するビジネス ロジックを保存します。 trelloservice.py py-trello : プログラムの単体テストを保存します。 tests : CLI 実装の単体テストを保存します。 test_cli.py : との対話のための単体テストを保存します。 test_trelloservice.py py-trello : プログラムのドキュメントを保存します。 README.md : パッケージの構成と要件を保存します pyproject.toml : 環境変数を保存します .env : バージョン管理中に無視する (追跡しない) ファイルを指定します。 .gitignore Python パッケージの公開の詳細については、 参照してください。 Geir Arne Hjelle 著の「オープンソース Python パッケージを PyPI に公開する方法」を 設定 始める前に、パッケージのセットアップについて触れてみましょう。 パッケージ内の ファイルから始めます。このファイルには、アプリ名やバージョンなどのパッケージの定数と変数が保存されます。この例では、次のものを初期化したいと考えています。 __init__.py アプリ名 バージョン SUCCESS および ERROR 定数 # trellocli/__init__.py __app_name__ = "trellocli" __version__ = "0.1.0" ( SUCCESS, TRELLO_WRITE_ERROR, TRELLO_READ_ERROR ) = range(3) ERRORS = { TRELLO_WRITE_ERROR: "Error when writing to Trello", TRELLO_READ_ERROR: "Error when reading from Trello" } ファイルに進むと、プログラムのメイン フローがここに保存されます。この例では、 に呼び出し可能な関数があると想定して、CLI プログラムのエントリ ポイントを保存します。 __main__.py cli.py # trellocli/__main__.py from trellocli import cli def main(): # we'll modify this later - after the implementation of `cli.py` pass if __name__ == "__main__": main() パッケージのセットアップが完了したので、 ファイル (メインのドキュメント) の更新を見てみましょう。従う必要がある特定の構造はありませんが、優れた README は次の内容で構成されます。 README.md 概要 インストールと要件 はじめにと使い方 さらに詳しく知りたい場合は、別の素晴らしい投稿を読んでください: How to Write a Good README by merlos このプロジェクトの README を次のように構成したいと思います <!--- README.md --> # Overview # Getting Started # Usage # Architecture ## Data Flow ## Tech Stack # Running Tests # Next Steps # References 現時点ではスケルトンをそのままにしておきます。これについては後で説明します。 次に、 に基づいてパッケージのメタデータを構成しましょう 公式ドキュメント # pyproject.toml [project] name = "trellocli_<YOUR_USERNAME>" version = "0.1.0" authors = [ { name = "<YOUR_NAME>", email = "<YOUR_EMAIL>" } ] description = "Program to modify your Trello boards from your computer's command line" readme = "README.md" requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] dependencies = [] [project.urls] "Homepage" = "" ユーザー名や名前など、変更する必要があるプレースホルダーがあることに注意してください。 話は変わりますが、ホームページの URL は今のところ空のままにしておきます。 GitHub に公開した後に変更を加えます。また、依存関係の部分は今のところ空のままにし、必要に応じて追加していきます。 リストの次は、API シークレットやキーなどの環境変数を保存する ファイルです。このファイルには機密情報が含まれているため、Git で追跡しないように注意することが重要です。 .env 私たちの場合、Trello 認証情報をここに保存します。 Trello でパワーアップを作成するには、 に従ってください。具体的には、 による使用法に基づいて、アプリケーションに OAuth を使用する予定であるため、Trello と通信するには次のものが必要になります。 このガイド py-trello API キー (アプリケーション用) API シークレット (アプリケーション用) トークン (データへのアクセスを許可するためのユーザーのトークン) API キーとシークレットを取得したら、それらを ファイルにそのまま保存します。 .env # .env TRELLO_API_KEY=<your_api_key> TRELLO_API_SECRET=<your_api_secret> 最後になりましたが、 あるテンプレート Python を使用してみましょう。これは、 ファイルが決して追跡されないようにするために重要であることに注意してください。ある時点で ファイルが追跡された場合、たとえ後の手順でファイルを削除したとしても、損害は発生し、悪意のある攻撃者が以前のファイルを追跡できるようになります。機密情報のパッチ。 ここに .gitignore .env .env セットアップが完了したので、変更を GitHub にプッシュしましょう。 で指定されているメタデータに応じて、それに応じてライセンスとホームページの URL を忘れずに更新してください。より良いコミットの書き方については、 参照してください。 pyproject.toml 「Write Better Commits, Build Better Projects」(Victoria Dye 著) を その他の注目すべきステップ: プロジェクトの virtualenv を構築する 単体テスト テストの作成を開始する前に、API を使用して作業しているため、API ダウンタイムのリスクなしでプログラムをテストできるように模擬テストを実装することに注意することが重要です。 Real Python による模擬テストに関する別の優れた記事は次のとおりです: Python での外部 API の模擬 機能要件に基づいて、私たちの主な関心事は、ユーザーが新しいカードを追加できるようにすることです。 のメソッドを参照します: 。これを行うには、 クラスから メソッドを呼び出す必要があります。List クラスは、 クラスの 関数から取得できます。 py-trello add_card List add_card Board get_list 要点はわかりました。最終目的地に到達するには、多くのヘルパー メソッドが必要です。それを言葉で言いましょう。 クライアントのトークンを取得するテスト ボードを取得するテスト ボードを取得するテスト ボードからリストを取得するテスト リストを取得するテスト ボードからラベルを取得するテスト ラベルを取得するテスト カードを追加するテスト カードにラベルを追加するテスト 単体テストを作成するときは、テストを可能な限り広範囲にわたって行う必要があることに注意することも重要です。つまり、エラーは適切に処理されますか?それは私たちのプログラムのあらゆる側面をカバーしていますか? ただし、このチュートリアルでは、成功事例のみを確認することで物事を簡略化します。 コードに入る前に、 ファイルを変更して、単体テストの作成/実行に必要な依存関係を含めましょう。 pyproject.toml # pyproject.toml [project] dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1" ] 次に、 virtualenv をアクティブにして 依存関係をインストールします。 pip install . それが完了したら、最後にテストをいくつか書いてみましょう。一般に、テストには、返されるモック化された応答、モック化された応答で戻り値を修正することによってテストしようとしている関数にパッチを適用し、最後に関数の呼び出しを含める必要があります。ユーザーのアクセス トークンを取得するサンプル テストは次のようになります。 # tests/test_trelloservice.py # module imports from trellocli import SUCCESS from trellocli.trelloservice import TrelloService from trellocli.models import * # dependencies imports # misc imports def test_get_access_token(mocker): """Test to check success retrieval of user's access token""" mock_res = GetOAuthTokenResponse( token="test", token_secret="test", status_code=SUCCESS ) mocker.patch( "trellocli.trelloservice.TrelloService.get_user_oauth_token", return_value=mock_res ) trellojob = TrelloService() res = trellojob.get_user_oauth_token() assert res.status_code == SUCCESS サンプル コードでは、 にまだ設定されていないモデルであることに注意してください。これは、よりクリーンなコードを作成するための構造を提供します。後でこれを実際に見ていきます。 GetOAuthTokenResponse models.py テストを実行するには、 を実行するだけです。テストがどのように失敗するかに注目してください。しかし、それは問題ありません。最終的にはうまくいきます。 python -m pytest 💡 自分でもっとテストを書いてみませんか?私のテストがどのようなものかを確認するには、 を参照してください。 チャレンジ コーナー このパッチ ここでは、 を構築しましょう。新しい依存関係、つまり ラッパーを追加することから始めます。 trelloservice py-trello # pyproject.toml dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1", "py-trello==0.19.0" ] もう一度、 依存関係をインストールします。 pip install . モデル さて、モデルを構築することから始めましょう - で期待される応答を制御します。この部分については、単体テストと ソース コードを参照して、予想される戻り値の種類を理解するのが最善です。 trelloservice py-trello たとえば、ユーザーのアクセス トークンを取得したいとします。 の 関数 ( ) を参照すると、戻り値は次のようなものになることがわかります。 py-trello create_oauth_token ソース コード # trellocli/models.py # module imports # dependencies imports # misc imports from typing import NamedTuple class GetOAuthTokenResponse(NamedTuple): token: str token_secret: str status_code: int 一方で、競合する命名規則に注意してください。たとえば、 モジュールには という名前のクラスがあります。この問題を回避するには、インポート時にエイリアスを指定します。 py-trello List # trellocli/models.py # dependencies imports from trello import List as Trellolist この機会を自由に利用して、プログラムのニーズに合わせてモデルを調整してください。たとえば、戻り値から 1 つの属性のみが必要な場合、その値を全体として保存するのではなく、戻り値から抽出することを期待するようにモデルをリファクタリングできます。 # trellocli/models.py class GetBoardName(NamedTuple): """Model to store board id Attributes id (str): Extracted board id from Board value type """ id: str 💡 自分でもっとモデルを書いてみませんか?私のモデルがどのように見えるかを確認するには、 を参照してください。 チャレンジ コーナー このパッチ ビジネスの論理 設定 モデルは終了です。正式に のコーディングを開始しましょう。繰り返しますが、作成した単体テストを参照する必要があります。たとえば、現在のテストのリストではサービスを完全にカバーしていないため、必要に応じて常にテストを返し、テストを追加します。 trelloservice 通常どおり、すべての import ステートメントを先頭の方に含めます。次に、予想どおり クラスとプレースホルダー メソッドを作成します。このアイデアは、 でサービスの共有インスタンスを初期化し、それに応じてそのメソッドを呼び出すというものです。さらに、私たちはスケーラビリティを目指しているため、広範囲にわたるカバレッジが必要になります。 TrelloService cli.py # trellocli/trelloservice.py # module imports from trellocli import TRELLO_READ_ERROR, TRELLO_WRITE_ERROR, SUCCESS from trellocli.models import * # dependencies imports from trello import TrelloClient # misc imports class TrelloService: """Class to implement the business logic needed to interact with Trello""" def __init__(self) -> None: pass def get_user_oauth_token() -> GetOAuthTokenResponse: pass def get_all_boards() -> GetAllBoardsResponse: pass def get_board() -> GetBoardResponse: pass def get_all_lists() -> GetAllListsResponse: pass def get_list() -> GetListResponse: pass def get_all_labels() -> GetAllLabelsResponse: pass def get_label() -> GetLabelResponse: pass def add_card() -> AddCardResponse: pass Ps は、今回テストを実行すると、テストがどのようにパスするかに注目してください。実際、これは私たちが正しい道を歩むことを確実にするのに役立ちます。ワークフローは、関数を拡張し、テストを実行し、合否を確認し、それに応じてリファクタリングする必要があります。 TrelloClient の認証と初期化 関数から始めましょう。ここで 関数を呼び出し、 を初期化するという考え方です。繰り返しになりますが、このような機密情報は ファイルにのみ保存する必要があることを強調し、 依存関係を使用して機密情報を取得します。 ファイルをそれに応じて変更した後、認証手順の実装を開始しましょう。 __init__ get_user_oauth_token TrelloClient .env python-dotenv pyproject.toml # trellocli/trelloservice.py class TrelloService: """Class to implement the business logic needed to interact with Trello""" def __init__(self) -> None: self.__load_oauth_token_env_var() self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) def __load_oauth_token_env_var(self) -> None: """Private method to store user's oauth token as an environment variable""" load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): res = self.get_user_oauth_token() if res.status_code == SUCCESS: dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_OAUTH_TOKEN", value_to_set=res.token ) else: print("User denied access.") self.__load_oauth_token_env_var() def get_user_oauth_token(self) -> GetOAuthTokenResponse: """Helper method to retrieve user's oauth token Returns GetOAuthTokenResponse: user's oauth token """ try: res = create_oauth_token() return GetOAuthTokenResponse( token=res["oauth_token"], token_secret=res["oauth_token_secret"], status_code=SUCCESS ) except: return GetOAuthTokenResponse( token="", token_secret="", status_code=TRELLO_AUTHORIZATION_ERROR ) この実装では、予見可能なエラー (たとえば、ユーザーが認証中に をクリックしたとき) を処理するためのヘルパー メソッドを作成しました。さらに、有効な応答が返されるまで再帰的にユーザーの承認を求めるように設定されています。実際には、ユーザーがアプリにアカウント データへのアクセスを承認しない限り、続行できないからです。 Deny 💡 に注意してください ?このエラーをパッケージ定数として宣言できますか?詳細については、「セットアップ」を参照してください。 チャレンジ コーナー TRELLO_AUTHORIZATION_ERROR ヘルパー関数 認証部分が完了したので、ユーザーの Trello ボードを取得することから始めて、ヘルパー関数に進みましょう。 # trellocli/trelloservice.py def get_all_boards(self) -> GetAllBoardsResponse: """Method to list all boards from user's account Returns GetAllBoardsResponse: array of user's trello boards """ try: res = self.__client.list_boards() return GetAllBoardsResponse( res=res, status_code=SUCCESS ) except: return GetAllBoardsResponse( res=[], status_code=TRELLO_READ_ERROR ) def get_board(self, board_id: str) -> GetBoardResponse: """Method to retrieve board Required Args board_id (str): board id Returns GetBoardResponse: trello board """ try: res = self.__client.get_board(board_id=board_id) return GetBoardResponse( res=res, status_code=SUCCESS ) except: return GetBoardResponse( res=None, status_code=TRELLO_READ_ERROR ) リスト (列) を取得するには、 の クラスをチェックアウトする必要があります。つまり、 値タイプの新しいパラメーターを受け入れる必要があります。 py-trello Board Board # trellocli/trelloservice.py def get_all_lists(self, board: Board) -> GetAllListsResponse: """Method to list all lists (columns) from the trello board Required Args board (Board): trello board Returns GetAllListsResponse: array of trello lists """ try: res = board.all_lists() return GetAllListsResponse( res=res, status_code=SUCCESS ) except: return GetAllListsResponse( res=[], status_code=TRELLO_READ_ERROR ) def get_list(self, board: Board, list_id: str) -> GetListResponse: """Method to retrieve list (column) from the trello board Required Args board (Board): trello board list_id (str): list id Returns GetListResponse: trello list """ try: res = board.get_list(list_id=list_id) return GetListResponse( res=res, status_code=SUCCESS ) except: return GetListResponse( res=None, status_code=TRELLO_READ_ERROR ) 💡 関数と 関数を自分で実装できますか? の クラスを修正します。私の実装がどのようなものかを確認するには、 を参照してください。 チャレンジコーナー get_all_labels get_label py-trello Board このパッチ 新規カード追加機能 最後になりましたが、私たちはこれまでずっと目指してきたこと、つまり新しいカードの追加をついに達成しました。ここでは、以前に宣言した関数のすべてを使用するわけではないことに注意してください。ヘルパー関数の目的は、スケーラビリティを向上させることです。 # trellocli/trelloservice.py def add_card( self, col: Trellolist, name: str, desc: str = "", labels: List[Label] = [] ) -> AddCardResponse: """Method to add a new card to a list (column) on the trello board Required Args col (Trellolist): trello list name (str): card name Optional Args desc (str): card description labels (List[Label]): list of labels to be added to the card Returns AddCardResponse: newly-added card """ try: # create new card new_card = col.add_card(name=name) # add optional description if desc: new_card.set_description(description=desc) # add optional labels if labels: for label in labels: new_card.add_label(label=label) return AddCardResponse( res=new_card, status_code=SUCCESS ) except: return AddCardResponse( res=new_card, status_code=TRELLO_WRITE_ERROR ) 🎉 これで完了です。必要に応じて README を更新し、コードを GitHub にプッシュすることを忘れないでください。 おめでとう!あなたは勝ちました。もう一度プレイしますか (y/N)? まとめ お付き合いいただきありがとうございます:) このチュートリアルを通じて、単体テストを作成するときにモックを実装すること、一貫性を保つためにモデルを構造化すること、ソース コードを読んで主要な機能を見つけること、サードパーティのラッパーを使用してビジネス ロジックを実装することを学習しました。 パート 2 に注目してください。そこでは、CLI プログラム自体の実装について詳しく説明します。 それまでの間、連絡を取り合いましょう👀 GitHub ソースコード: https://github.com/elainechan01/trellocli