paint-brush
如何为 Trello Board 管理创建 Python CLI 程序(第 2 部分)经过@elainechan01
1,672 讀數
1,672 讀數

如何为 Trello Board 管理创建 Python CLI 程序(第 2 部分)

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

太長; 讀書

关于如何为 Trello Board Management 创建 Python CLI 程序的教程系列的第 2 部分重点介绍如何为 CLI 命令和 Python 包分发编写业务逻辑
featured image - 如何为 Trello Board 管理创建 Python CLI 程序(第 2 部分)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

到目前为止,我们已经远远超出了主要的石头剪刀布学校项目 - 让我们直接进入它。


通过本教程我们将实现什么目标?

如何为 Trello Board 管理创建 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定义为包名称,并在__main__脚本中定义了main函数,该函数存储在trellocli模块中,将在运行时执行。


现在我们软件的 CLI 部分已经设置完毕,让我们修改trelloservice模块以更好地为我们的 CLI 程序提供服务。您还记得,我们的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函数,让它不再递归提示用户授权?提示:递归函数是调用自身的函数。


转到authorizeis_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 Board 管理创建 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


单元测试

正如第一部分中提到的,我们不会在本教程中广泛讨论单元测试,所以让我们只使用必要的元素:


  • 测试配置访问
  • 测试配置 trello board
  • 测试创建新的 trello 卡
  • 测试显示 trello board 详细信息
  • 测试显示 trello board 详细信息(详细视图)


这个想法是模拟一个命令行解释器,就像一个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模块 - 对于所有命令组(配置、创建),它们的业务逻辑将存储在自己的单独文件中,以获得更好的可读性。


在本模块中,我们将存储list命令。深入研究该命令,我们知道我们想要实现以下选项:


  • board_name:如果之前未设置config board则需要该名称
  • 详细:以详细视图显示


从 board_name 必需选项开始,有几种方法可以实现此目的,其中之一是使用回调函数(有关更多信息,请参阅官方文档)或仅使用默认环境变量。然而,对于我们的用例,让我们通过在不满足条件时引发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中是如何定义的吗?


让我们快进一点并初始化createconfig命令组。为此,我们只需在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命令组吗?请随意参考此补丁寻求帮助: 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 board 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

我们将使用buildtwine工具来构建和发布我们的包。在终端中运行以下命令来为您的包创建源存档和轮子:

 python -m build


确保您已经在 TestPyPI 上设置了帐户,然后运行以下命令

twine upload -r testpypi dist/*


系统会提示您输入用户名和密码。由于启用了两因素身份验证,您将需要使用 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 包集成的精彩文章。但现在,坐下来享受你的最新成就吧🎉。当 GitHub Actions 工作流程将您的包分发到 PyPI 时,请随意观看神奇的揭秘。


包起来

这是一篇很长的文章😓。通过本教程,您学习了使用Typer模块将软件转换为 CLI 程序并将包分发到 PyPI。为了更深入地了解,您学习了定义命令和命令组、开发交互式 CLI 会话,并涉足键盘中断等常见 CLI 场景。


你绝对是一个能坚持到底的奇才。您愿意加入我的第 3 部分吗?我们将在其中实现可选功能?