免責事項: このチュートリアルは、読者が Python、API、Git、単体テストの基礎知識を持っていることを前提としています。
最高にクールなアニメーションを備えたさまざまな CLI ソフトウェアに出会ったので、「ミニマルな」じゃんけん学校プロジェクトをアップグレードできるだろうか、と疑問に思いました。
こんにちは、遊びましょう!ファイターを選択してください (ジャンケン): ロック
Wikipedia に記載されているように、「コマンドライン インターフェイス (CLI) は、ユーザーまたはクライアントからのコマンドと、テキスト行の形式でのデバイスまたはプログラムからの応答によって、デバイスまたはコンピューター プログラムと対話する手段です。」
言い換えれば、CLI プログラムは、ユーザーがコマンド ラインを使用して、実行命令を提供することでプログラムと対話するプログラムです。
日常的なソフトウェアの多くは、CLI プログラムとしてラップされています。たとえば、 vim
テキスト エディターを考えてみましょう。このツールは、UNIX システムに付属しており、ターミナルでvim <FILE>
実行するだけでアクティブ化できます。
Google Cloud CLIに関して、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 はset
、 list
などになります… ( GROUP は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 プログラムの草案を作成しましょう。
機能要件
非機能要件
オプションの要件
上記に基づいて、CLI プログラムのコマンドとオプションを次のように形式化できます。
Ps 最後の 2 つの列については心配しないでください。それについては後ほど説明します…
私たちの技術スタックに関しては、これに固執します。
単体テスト
トレロ
CLI
ユーティリティ (その他)
このプロジェクトは部分的に取り組んでいきます。期待できることの一部を以下に示します。
パート1
py-trello
ビジネスロジックの実装パート2
パート 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
__init__.py
: パッケージのルートを表し、フォルダーを Python パッケージとして準拠させます__main__.py
: エントリ ポイントを定義し、ユーザーが-m
フラグを使用してファイル パスを指定せずにモジュールを実行できるようにします。たとえば、 python -m <module_name>
でpython -m <parent_folder>/<module_name>.py
を置き換えます。models.py
: グローバルに使用されるクラス (API 応答が準拠すると予想されるモデルなど) を格納します。cli.py
: CLI コマンドとオプションのビジネス ロジックを保存します。trelloservice.py
: py-trello
と対話するビジネス ロジックを保存します。tests
: プログラムの単体テストを保存します。test_cli.py
: CLI 実装の単体テストを保存します。test_trelloservice.py
: py-trello
との対話のための単体テストを保存します。README.md
: プログラムのドキュメントを保存します。pyproject.toml
: パッケージの構成と要件を保存します.env
: 環境変数を保存します.gitignore
: バージョン管理中に無視する (追跡しない) ファイルを指定します。
Python パッケージの公開の詳細については、 Geir Arne Hjelle 著の「オープンソース Python パッケージを PyPI に公開する方法」を参照してください。
始める前に、パッケージのセットアップについて触れてみましょう。
パッケージ内の__init__.py
ファイルから始めます。このファイルには、アプリ名やバージョンなどのパッケージの定数と変数が保存されます。この例では、次のものを初期化したいと考えています。
# 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" }
__main__.py
ファイルに進むと、プログラムのメイン フローがここに保存されます。この例では、 cli.py
に呼び出し可能な関数があると想定して、CLI プログラムのエントリ ポイントを保存します。
# 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.md
ファイル (メインのドキュメント) の更新を見てみましょう。従う必要がある特定の構造はありませんが、優れた README は次の内容で構成されます。
さらに詳しく知りたい場合は、別の素晴らしい投稿を読んでください: 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 シークレットやキーなどの環境変数を保存する.env
ファイルです。このファイルには機密情報が含まれているため、Git で追跡しないように注意することが重要です。
私たちの場合、Trello 認証情報をここに保存します。 Trello でパワーアップを作成するには、このガイドに従ってください。具体的には、 py-trello
による使用法に基づいて、アプリケーションに OAuth を使用する予定であるため、Trello と通信するには次のものが必要になります。
API キーとシークレットを取得したら、それらを.env
ファイルにそのまま保存します。
# .env TRELLO_API_KEY=<your_api_key> TRELLO_API_SECRET=<your_api_secret>
最後になりましたが、ここにあるテンプレート Python .gitignore
を使用してみましょう。これは、 .env
ファイルが決して追跡されないようにするために重要であることに注意してください。ある時点で.env
ファイルが追跡された場合、たとえ後の手順でファイルを削除したとしても、損害は発生し、悪意のある攻撃者が以前のファイルを追跡できるようになります。機密情報のパッチ。
セットアップが完了したので、変更を GitHub にプッシュしましょう。 pyproject.toml
で指定されているメタデータに応じて、それに応じてライセンスとホームページの URL を忘れずに更新してください。より良いコミットの書き方については、 「Write Better Commits, Build Better Projects」(Victoria Dye 著) を参照してください。
その他の注目すべきステップ:
テストの作成を開始する前に、API を使用して作業しているため、API ダウンタイムのリスクなしでプログラムをテストできるように模擬テストを実装することに注意することが重要です。 Real Python による模擬テストに関する別の優れた記事は次のとおりです: Python での外部 API の模擬
機能要件に基づいて、私たちの主な関心事は、ユーザーが新しいカードを追加できるようにすることです。 py-trello
のメソッドを参照します: add_card 。これを行うには、 List
クラスからadd_card
メソッドを呼び出す必要があります。List クラスは、 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 は、今回テストを実行すると、テストがどのようにパスするかに注目してください。実際、これは私たちが正しい道を歩むことを確実にするのに役立ちます。ワークフローは、関数を拡張し、テストを実行し、合否を確認し、それに応じてリファクタリングする必要があります。
__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