Мы уже вышли за рамки основного школьного проекта «камень-ножницы-бумага» — давайте углубимся прямо в него. Чего мы достигнем с помощью этого урока? В мы успешно создали бизнес-логику для взаимодействия с Trello SDK. разделе «Как создать программу CLI Python для управления доской Trello (часть 1)» Вот краткий обзор архитектуры нашей программы CLI: В этом уроке мы рассмотрим, как преобразовать наш проект в программу CLI, уделяя особое внимание функциональным и нефункциональным требованиям. С другой стороны, мы также научимся распространять нашу программу в виде пакета на . PyPI Давайте начнем Структура папок Ранее нам удалось настроить скелет для размещения нашего модуля . На этот раз мы хотим реализовать папку с модулями для разных функций, а именно: trelloservice cli конфигурация доступ список Идея состоит в том, что для каждой группы команд ее команды будут храниться в отдельном модуле. Что касается команды , мы сохраним ее в основном файле CLI, поскольку она не принадлежит ни к одной группе команд. list С другой стороны, давайте займемся очисткой структуры наших папок. Говоря более конкретно, мы должны начать учитывать масштабируемость программного обеспечения, гарантируя, что каталоги не будут загромождены. Вот предложение по структуре наших папок: 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 Настраивать Начнем с изменения нашего файла точки входа . Что касается самого импорта, поскольку мы решили хранить связанные модули в отдельных подпапках, нам придется учесть такие изменения. С другой стороны, мы также предполагаем, что основной модуль CLI, , имеет экземпляр , которое мы можем запустить. __main__.py 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 нашего программного обеспечения настроена, давайте изменим наш модуль , чтобы он лучше обслуживал нашу программу 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 Следует понимать, что разница между и заключается в том, что — это , которая служит для хранения токена авторизации в качестве переменной среды, тогда как является общедоступной функцией и пытается получить все необходимые учетные данные и инициализировать клиент Trello. __load_oauth_token_env_var authorize __load_oauth_token_env_var внутренняя функция authorize 💡Обратите внимание, что наша функция возвращает тип данных . Можете ли вы реализовать модель с атрибутом ? Обратитесь к (подсказка: посмотрите, как мы создавали модели). Уголок задач authorize AuthorizeResponse status_code части 1 статьи «Как создать программу CLI Python для управления доской Trello» Наконец, давайте создадим экземпляр одноэлементного объекта в нижней части модуля. Не стесняйтесь обратиться к этому патчу, чтобы увидеть, как выглядит полный код: TrelloService trello-cli-kit. # trellocli/trelloservice.py trellojob = TrelloService() Наконец, мы хотим инициализировать некоторые пользовательские исключения, которые будут использоваться во всей программе. Это отличается от , определенных в нашем инициализаторе, поскольку эти исключения являются подклассами и действуют как типичные пользовательские исключения, тогда как служат скорее постоянными значениями, начиная с 0. ERRORS BaseException ERRORS Давайте сведем наши исключения к минимуму и рассмотрим некоторые распространенные случаи использования, в первую очередь: Ошибка чтения: возникает при ошибке чтения из 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 Давайте вместе проработаем первый тест, то есть настроим доступ. Помните, что мы проверяем, правильно ли выполняется функция. Для этого мы проверим ответ системы и то, является ли код выхода , то есть 0. Вот отличная статья RedHat о . success том, что такое коды выхода и как система использует их для взаимодействия с процессами # 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 Поймите, что это будет наш основной модуль — для всех групп команд (config, create) их бизнес-логика будет храниться в отдельном файле для лучшей читаемости. cli В этом модуле мы будем хранить нашу команду . Углубляясь в команду, мы знаем, что хотим реализовать следующие параметры: list board_name: требуется, если ранее не была установлена. config board подробный: отображение в подробном виде Начиная с обязательной опции board_name, есть несколько способов добиться этого, один из них — использование функции обратного вызова (для получения дополнительной информации см. ) или простое использование переменной среды по умолчанию. Однако в нашем случае давайте сделаем это проще, вызвав собственное исключение , если условия не выполняются. официальную документацию InvalidUserInputError Чтобы создать команду, давайте начнем с определения параметров. В Typer, как упоминается в их , ключевыми ингредиентами для определения опции будут: официальной документации Тип данных Вспомогательный текст Значение по умолчанию Например, чтобы создать вариант со следующими условиями: detailed Тип данных: логическое значение Вспомогательный текст: «Включить подробный просмотр» Значение по умолчанию: Нет Наш код будет выглядеть так: detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None В целом, чтобы определить команду с необходимыми параметрами, мы будем рассматривать как функцию Python, а ее параметры — как обязательные параметры. list list # 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 При настройке соответствующей платы мы попытаемся настроить плату Trello для использования на основе полученного . Таким образом, мы хотим охватить следующие варианты использования board_id Получение , если было явно указано путем проверки совпадения с помощью функции в trellojob. board_id board_name get_all_boards Получение , хранящегося как переменная среды, если опция не использовалась. board_id board_name Данные, которые мы будем отображать, будут отформатированы с использованием функции из пакета. Для получения дополнительной информации о обратитесь к их . Table rich 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...") Чтобы увидеть наше программное обеспечение в действии, просто запустите в терминале. По умолчанию модуль Typer самостоятельно заполняет выходные данные команды . Обратите внимание, как мы можем назвать в качестве имени пакета — помните, как это было ранее определено в нашем ? python -m trellocli --help --help trellocli pyproject.toml Давайте немного перенесемся вперед и инициализируем группы команд и . Для этого мы просто воспользуемся функцией для нашего объекта . Идея состоит в том, что у группы команд будет свой собственный объект , и мы просто добавим его в главное в вместе с именем группы команд и вспомогательным текстом. Это должно выглядеть примерно так create config add_typer app app app cli.py # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") 💡Можете ли вы импортировать группу команд самостоятельно? Не стесняйтесь обращаться за помощью к этому патчу: Уголок вызовов create trello-cli-kit. Подкоманды Чтобы настроить группу команд для , мы будем хранить соответствующие команды в отдельном модуле. Настройка аналогична настройке , но требует создания экземпляра объекта Typer. Что касается команд, то нам также хотелось бы придерживаться необходимости использования пользовательских исключений. Дополнительная тема, которую мы хотим затронуть, — это когда пользователь нажимает или, другими словами, прерывает процесс. Причина, по которой мы не рассмотрели это для нашей команды , заключается в том, что разница здесь в том, что группа команд состоит из интерактивных команд. Основное различие между интерактивными командами заключается в том, что они требуют постоянного взаимодействия с пользователем. Конечно, скажем, что наша прямая команда выполняется долго. Также рекомендуется обрабатывать потенциальные прерывания клавиатуры. create cli.py Ctrl + C list config Начиная с команды , мы, наконец, будем использовать функцию , созданную в нашем . Поскольку функция самостоятельно обрабатывает конфигурацию, нам нужно будет только проверить выполнение процесса. access authorize TrelloService 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 простое меню терминала Проверьте авторизацию Получить все доски Trello из учетной записи пользователя. Отображение меню терминала с одним выбором для досок Trello Установите выбранный идентификатор доски Trello в качестве переменной среды. # 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 🎉 Подробное объяснение можно найти в этом замечательном руководстве по упаковке Python от Рамита Миттала. Конфигурация метаданных Последняя деталь, которая нам нужна для нашего — это указать, какой модуль хранит сам пакет. В нашем случае это будет . Вот метаданные, которые нужно добавить: pyproject.toml trellocli # pyproject.toml [tool.setuptools] packages = ["trellocli"] Что касается нашего , то полезно предоставить какое-то руководство, будь то рекомендации по использованию или начало работы. Если вы включили изображения в свой , вам следует использовать его абсолютный URL-адрес, который обычно имеет следующий формат: README.md README.md https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image> ТестПиПИ Мы будем использовать инструменты и для сборки и публикации нашего пакета. Запустите следующую команду в своем терминале, чтобы создать исходный архив и колесо для вашего пакета: build twine python -m build Убедитесь, что у вас уже настроена учетная запись в TestPyPI, и выполните следующую команду twine upload -r testpypi dist/* Вам будет предложено ввести имя пользователя и пароль. Поскольку включена двухфакторная аутентификация, вам потребуется использовать токен API (дополнительную информацию о том, как получить токен API TestPyPI: ). Просто введите следующие значения: ссылка на документацию имя пользователя: жетон пароль: <ваш токен TestPyPI> Как только это будет завершено, вы сможете перейти в TestPyPI, чтобы проверить свой недавно распространяемый пакет! Настройка GitHub Цель — использовать GitHub как средство постоянного обновления новых версий вашего пакета на основе тегов. Сначала перейдите на вкладку в рабочем процессе GitHub и выберите новый рабочий процесс. Мы будем использовать рабочий процесс , созданный GitHub Actions. Обратите внимание, что рабочий процесс требует чтения секретов репозитория? Убедитесь, что вы сохранили свой токен PyPI под указанным именем (получение токена API PyPI аналогично получению токена TestPyPI). Actions Publish Python Package Как только рабочий процесс будет создан, мы отправим наш код в тег v1.0.0. Дополнительную информацию о синтаксисе именования версий можно найти в отличном объяснении от Py-Pkgs: ссылка на документацию. Просто запустите обычные команды , и . Затем создайте тег для вашего последнего коммита, выполнив следующую команду (для получения дополнительной информации о тегах: ). pull add commit ссылка на документацию git tag <tagname> HEAD Наконец, отправьте новый тег в удаленный репозиторий. git push <remote name> <tag name> Вот отличная статья , если вы хотите узнать больше. А пока расслабьтесь и наслаждайтесь своим последним достижением 🎉. Не стесняйтесь наблюдать за тем, как разворачивается волшебство в рабочем процессе GitHub Actions при распространении вашего пакета в PyPI. Кароля Хоросина об интеграции CI/CD с вашим пакетом Python Заворачивать Это было долго 😓. Благодаря этому руководству вы научились преобразовывать свое программное обеспечение в программу CLI с помощью модуля и распространять свой пакет в PyPI. Чтобы погрузиться глубже, вы научились определять команды и группы команд, разрабатывать интерактивный сеанс CLI и работать с распространенными сценариями CLI, такими как прерывание клавиатуры. Typer Вы были настоящим волшебником, выдержав все это. Не присоединитесь ли вы ко мне в части 3, где мы реализуем дополнительные функции?