嗨,我们来玩吧!选择你的战士(石头、剪刀、布):石头
许多日常软件都包装为 CLI 程序。以vim
文本编辑器为例 - 这是任何 UNIX 系统附带的工具,只需在终端中运行vim <FILE>
即可激活它。
例如,当我们想要在核心部分设置project
属性时,我们运行gcloud config set project <PROJECT_ID>
争论不休 | 的内容 |
---|---|
精氨酸0 | 可儿 |
精氨酸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
是选项。
另一个例子是,当我们想要在特定项目的计算部分设置区域属性时,我们运行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
标志,则该标志是必需的。
短选项以-
开头,后跟单个字母数字字符,而长选项以--
开头,后跟多个字符。当用户确定自己想要什么时,可以将短选项视为快捷方式,而长选项则更具可读性。
你选择了摇滚!计算机现在将做出选择。
您的团队使用 Trello 来跟踪项目的问题和进度。您的团队正在寻找一种更简化的与董事会交互的方式 - 类似于通过终端创建新的 GitHub 存储库。该团队请您创建一个 CLI 程序,其基本要求是能够将新卡添加到看板的“待办事项”列中。
功能要求
非功能性需求
可选要求
Ps 不要担心最后两列,我们稍后会了解......
单元测试
特雷洛
命令行界面
实用程序(杂项)
第1部分
py-trello
业务逻辑的实现第2部分
第三部分
电脑选择了剪刀!让我们看看谁赢得了这场战斗……
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
:指定版本控制期间要忽略(不跟踪)的文件
从包中的__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 程序入口点,假设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.md
文件(主要文档)。我们没有必须遵循的特定结构,但一个好的自述文件应包含以下内容:
<!--- 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" = ""
列表中的下一个是.env
文件,我们在其中存储 API 机密和密钥等环境变量。请务必注意,Git 不应跟踪此文件,因为它包含敏感信息。
在我们的例子中,我们将在此处存储 Trello 凭据。要在 Trello 中创建 Power-Up,请遵循。更具体地说,根据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。有关如何编写更好的提交的参考:
基于功能需求,我们主要关心的是允许用户添加新卡。引用py-trello
中的方法: 。为此,我们必须从List
类调用add_card
方法,该方法可以从Board
类的get_list
函数中检索,而 Board 类可以检索该方法......
在深入研究代码之前,让我们修改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
# trellocli/models.py class GetBoardName(NamedTuple): """Model to store board id Attributes id (str): Extracted board id from Board value type """ id: str
挑战角💡你能尝试自己编写更多模型吗?请随意参考来看看我的模型是什么样子
模型下来了,让我们正式开始编写trelloservice
代码。同样,我们应该参考我们创建的单元测试 - 假设当前的测试列表没有提供服务的完整覆盖,总是在需要时返回并添加更多测试。
像往常一样,将所有导入语句包含在顶部。然后按预期创建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
请注意,这一次当我们运行测试时,我们的测试将如何通过。事实上,这将有助于我们确保坚持正确的轨道。工作流程应该是扩展我们的功能、运行我们的测试、检查通过/失败并相应地进行重构。
让我们从__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
吗?您可以将此错误声明为包常量吗?请参阅设置以获取更多信息
# 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 )
恭喜!你赢了。再次玩(是/否)?