paint-brush
Trello 보드 관리를 위한 Python CLI 프로그램을 만드는 방법(2부)~에 의해@elainechan01
1,777 판독값
1,777 판독값

Trello 보드 관리를 위한 Python CLI 프로그램을 만드는 방법(2부)

~에 의해 Elaine Yun Ru Chan27m2023/11/07
Read on Terminal Reader

너무 오래; 읽다

CLI 명령 및 Python 패키지 배포를 위한 비즈니스 로직을 작성하는 방법에 초점을 맞춘 Trello 보드 관리용 Python CLI 프로그램을 만드는 방법에 대한 튜토리얼 시리즈의 2부
featured image - Trello 보드 관리를 위한 Python CLI 프로그램을 만드는 방법(2부)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

우리는 이제 주요 가위바위보 학교 프로젝트를 훨씬 넘어섰습니다. 바로 시작해 보겠습니다.


이 튜토리얼을 통해 우리는 무엇을 얻을 수 있나요?

Trello 보드 관리를 위한 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 패키지 이름으로 정의했으며 trellocli 모듈에 저장된 __main__ 스크립트의 main 함수가 런타임 중에 실행됩니다.


이제 소프트웨어의 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 함수를 수정할 수 있나요? 힌트: 재귀 함수는 자신을 호출하는 함수입니다.


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 보드 관리를 위한 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


단위 테스트

1부에서 언급했듯이 이 튜토리얼에서는 단위 테스트를 광범위하게 다루지 않으므로 필요한 요소만 다루겠습니다.


  • 액세스 구성 테스트
  • 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 블록을 사용하면 프로그램이 치명적인 충돌을 방지할 수 있습니다.
  • 사용할 적절한 보드를 구성할 때 검색된 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 create 그룹을 직접 가져올 수 있나요? 도움이 필요하면 언제든지 이 패치를 참조하세요: trello-cli-kit


하위 명령

create 를 위한 명령 그룹을 설정하기 위해 해당 명령을 자체 모듈에 저장합니다. 설정은 Typer 객체를 인스턴스화해야 하는 cli.py 의 설정과 유사합니다. 명령에 관해서는 사용자 정의 예외를 사용해야 한다는 요구 사항도 준수하고 싶습니다. 우리가 다루고 싶은 추가 주제는 사용자가 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 보드 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 목록: 단일 선택
  • 카드 이름: 텍스트
  • [선택] 카드 설명: 텍스트
  • [선택사항] 라벨: 다중 선택
  • 확인: y/N


사용자가 목록에서 선택해야 하는 모든 프롬프트에 대해 이전과 같이 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 Packaging에 대한 훌륭한 튜토리얼을 확인하세요.


메타데이터 구성

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/*


사용자 이름과 비밀번호를 입력하라는 메시지가 표시됩니다. 2단계 인증이 활성화되어 있으므로 API 토큰을 사용해야 합니다(TestPyPI API 토큰을 얻는 방법에 대한 자세한 내용은 문서 링크 ). 다음 값을 입력하면 됩니다.


  • 사용자 이름: 토큰
  • 비밀번호: <TestPyPI 토큰>


완료되면 TestPyPI로 이동하여 새로 배포된 패키지를 확인할 수 있습니다!


GitHub 설정

목표는 태그를 기반으로 패키지의 새 버전을 지속적으로 업데이트하는 수단으로 GitHub를 활용하는 것입니다.


먼저 GitHub 워크플로의 Actions 탭으로 이동하여 새 워크플로를 선택하세요. GitHub Actions에서 생성된 Publish Python Package 워크플로를 사용하겠습니다. 워크플로에서 저장소 비밀을 읽어야 하는 방법에 주목하세요. PyPI 토큰을 지정된 이름으로 저장했는지 확인하세요(PyPI API 토큰 획득은 TestPyPI 획득과 유사합니다).


워크플로가 생성되면 코드를 v1.0.0 태그에 푸시하겠습니다. 버전 명명 구문에 대한 자세한 내용은 Py-Pkgs의 훌륭한 설명을 참조하세요. 문서 링크


일반적인 pull , addcommit 명령을 실행하기만 하면 됩니다. 다음으로, 다음 명령을 실행하여 최신 커밋에 대한 태그를 생성합니다(태그에 대한 자세한 내용: 문서 링크 ).

 git tag <tagname> HEAD


마지막으로 새 태그를 원격 저장소에 푸시하세요.

 git push <remote name> <tag name>


더 자세히 알아보고 싶다면 CI/CD를 Python 패키지와 통합하는 방법에 대한 Karol Horosin 의 훌륭한 기사를 참조하세요. 하지만 지금은 편안히 앉아 최신 성과를 즐겨보세요 🎉. PyPI에 패키지를 배포할 때 GitHub Actions 워크플로로 마법이 풀리는 모습을 자유롭게 지켜보세요.


요약

😓 말이 길었습니다. 이 튜토리얼을 통해 Typer 모듈을 사용하여 소프트웨어를 CLI 프로그램으로 변환하고 패키지를 PyPI에 배포하는 방법을 배웠습니다. 더 자세히 알아보기 위해 명령 및 명령 그룹을 정의하고, 대화형 CLI 세션을 개발하고, 키보드 중단과 같은 일반적인 CLI 시나리오를 다루는 방법을 배웠습니다.


당신은 이 모든 것을 이겨낸 절대적인 마법사였습니다. 선택적 기능을 구현하는 파트 3에 참여하지 않으시겠습니까?