paint-brush
Как создать программу CLI на Python для управления доской Trello (часть 2)к@elainechan01
1,780 чтения
1,780 чтения

Как создать программу CLI на Python для управления доской Trello (часть 2)

к Elaine Yun Ru Chan27m2023/11/07
Read on Terminal Reader

Слишком долго; Читать

Часть 2 серии руководств «Как создать программу Python CLI для управления доской Trello», посвященная написанию бизнес-логики для команд CLI и распространению пакетов Python.
featured image - Как создать программу CLI на Python для управления доской Trello (часть 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

Мы уже вышли за рамки основного школьного проекта «камень-ножницы-бумага» — давайте углубимся прямо в него.


Чего мы достигнем с помощью этого урока?

В разделе «Как создать программу CLI Python для управления доской Trello (часть 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 так, чтобы она не запрашивала рекурсивно авторизацию пользователя? Подсказка: рекурсивная функция — это функция, которая вызывает саму себя.


Переходя к вспомогательным функциям 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


Следует понимать, что разница между __load_oauth_token_env_var и authorize заключается в том, что __load_oauth_token_env_var — это внутренняя функция , которая служит для хранения токена авторизации в качестве переменной среды, тогда как authorize является общедоступной функцией и пытается получить все необходимые учетные данные и инициализировать клиент Trello.


Уголок задач 💡Обратите внимание, что наша функция authorize возвращает тип данных AuthorizeResponse . Можете ли вы реализовать модель с атрибутом status_code ? Обратитесь к части 1 статьи «Как создать программу CLI Python для управления доской Trello» (подсказка: посмотрите, как мы создавали модели).


Наконец, давайте создадим экземпляр одноэлементного объекта 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


Модульные тесты

Как упоминалось в части I, в этом руководстве мы не будем подробно рассматривать модульные тесты, поэтому давайте работать только с необходимыми элементами:


  • Тест для настройки доступа
  • Тест для настройки доски Trello
  • Тестирование создания новой карты Trello
  • Тест для отображения сведений о доске Trello
  • Тест для отображения сведений о доске Trello (подробное представление)


Идея состоит в том, чтобы высмеять интерпретатор командной строки, например 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 — для всех групп команд (config, create) их бизнес-логика будет храниться в отдельном файле для лучшей читаемости.


В этом модуле мы будем хранить нашу команду 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 , мы можем предотвратить фатальные сбои нашей программы.
  • При настройке соответствующей платы мы попытаемся настроить плату Trello для использования на основе полученного board_id . Таким образом, мы хотим охватить следующие варианты использования
    • Получение board_id , если board_name было явно указано путем проверки совпадения с помощью функции get_all_boards в trellojob.
    • Получение 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...")


Чтобы увидеть наше программное обеспечение в действии, просто запустите python -m trellocli --help в терминале. По умолчанию модуль Typer самостоятельно заполняет выходные данные команды --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.


Подкоманды

Чтобы настроить группу команд для create , мы будем хранить соответствующие команды в отдельном модуле. Настройка аналогична настройке cli.py , но требует создания экземпляра объекта Typer. Что касается команд, то нам также хотелось бы придерживаться необходимости использования пользовательских исключений. Дополнительная тема, которую мы хотим затронуть, — это когда пользователь нажимает 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"]


Что касается нашего README.md , то полезно предоставить какое-то руководство, будь то рекомендации по использованию или начало работы. Если вы включили изображения в свой README.md , вам следует использовать его абсолютный URL-адрес, который обычно имеет следующий формат:

 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 как средство постоянного обновления новых версий вашего пакета на основе тегов.


Сначала перейдите на вкладку Actions в рабочем процессе GitHub и выберите новый рабочий процесс. Мы будем использовать рабочий процесс Publish Python Package , созданный GitHub Actions. Обратите внимание, что рабочий процесс требует чтения секретов репозитория? Убедитесь, что вы сохранили свой токен PyPI под указанным именем (получение токена API PyPI аналогично получению токена TestPyPI).


Как только рабочий процесс будет создан, мы отправим наш код в тег v1.0.0. Дополнительную информацию о синтаксисе именования версий можно найти в отличном объяснении от Py-Pkgs: ссылка на документацию.


Просто запустите обычные команды pull , add и commit . Затем создайте тег для вашего последнего коммита, выполнив следующую команду (для получения дополнительной информации о тегах: ссылка на документацию ).

 git tag <tagname> HEAD


Наконец, отправьте новый тег в удаленный репозиторий.

 git push <remote name> <tag name>


Вот отличная статья Кароля Хоросина об интеграции CI/CD с вашим пакетом Python , если вы хотите узнать больше. А пока расслабьтесь и наслаждайтесь своим последним достижением 🎉. Не стесняйтесь наблюдать за тем, как разворачивается волшебство в рабочем процессе GitHub Actions при распространении вашего пакета в PyPI.


Заворачивать

Это было долго 😓. Благодаря этому руководству вы научились преобразовывать свое программное обеспечение в программу CLI с помощью модуля Typer и распространять свой пакет в PyPI. Чтобы погрузиться глубже, вы научились определять команды и группы команд, разрабатывать интерактивный сеанс CLI и работать с распространенными сценариями CLI, такими как прерывание клавиатуры.


Вы были настоящим волшебником, выдержав все это. Не присоединитесь ли вы ко мне в части 3, где мы реализуем дополнительные функции?