paint-brush
Como criar um programa Python CLI para gerenciamento do Trello Board (Parte 2)por@elainechan01
1,672 leituras
1,672 leituras

Como criar um programa Python CLI para gerenciamento do Trello Board (Parte 2)

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

Muito longo; Para ler

Parte 2 da série de tutoriais sobre como criar um programa CLI Python para gerenciamento do Trello Board com foco em como escrever lógica de negócios para comandos CLI e distribuição de pacotes Python
featured image - Como criar um programa Python CLI para gerenciamento do Trello Board (Parte 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

Já estamos muito além do projeto escolar básico de pedra-papel-tesoura - vamos mergulhar direto nele.


O que alcançaremos com este tutorial?

Em Como criar um programa Python CLI para gerenciamento do Trello Board (Parte 1) , criamos com sucesso a lógica de negócios para interagir com o Trello SDK.


Aqui está uma rápida recapitulação da arquitetura do nosso programa CLI:

Visualização detalhada da tabela da estrutura CLI com base nos requisitos


Neste tutorial, veremos como transformar nosso projeto em um programa CLI, focando nos requisitos funcionais e não funcionais.


Por outro lado, também aprenderemos como distribuir nosso programa como um pacote no PyPI .


Vamos começar


Estrutura de pastas

Anteriormente, conseguimos configurar um esqueleto para hospedar nosso módulo trelloservice . Desta vez, queremos implementar uma pasta cli com módulos para diferentes funcionalidades, nomeadamente:


  • configuração
  • acesso
  • lista


A ideia é que, para cada grupo de comandos, seus comandos sejam armazenados em um módulo próprio. Quanto ao comando list , iremos armazená-lo no arquivo CLI principal, pois não pertence a nenhum grupo de comandos.


Por outro lado, vamos tentar limpar nossa estrutura de pastas. Mais especificamente, devemos começar a levar em conta a escalabilidade do software, garantindo que os diretórios não estejam desordenados.


Aqui está uma sugestão sobre nossa estrutura de pastas:

 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


Observe como existe a pasta assets ? Isso será usado para armazenar ativos relacionados para nosso README , embora exista uma pasta shared recém-implementada em trellocli , vamos usá-la para armazenar módulos a serem usados em todo o software.


Configurar

Vamos começar modificando nosso arquivo de ponto de entrada, __main__.py . Olhando para a importação em si, como decidimos armazenar os módulos relacionados em suas próprias subpastas, teremos que acomodar tais alterações. Por outro lado, também estamos assumindo que o módulo CLI principal, cli.py , possui uma instância app que podemos executar.

 # 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()


Avance rapidamente para nosso arquivo cli.py ; armazenaremos nossa instância app aqui. A ideia é inicializar um objeto Typer para ser compartilhado pelo software.

 # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer()


Seguindo em frente com esse conceito, vamos modificar nosso pyproject.toml para especificar nossos scripts de linha de comando. Aqui, forneceremos um nome para nosso pacote e definiremos o ponto de entrada.

 # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main"


Com base no exemplo acima, definimos trellocli como o nome do pacote e a função main no script __main__ , que está armazenado no módulo trellocli , será executada durante o tempo de execução.


Agora que a parte CLI do nosso software está configurada, vamos modificar nosso módulo trelloservice para melhor atender nosso programa CLI. Como você deve lembrar, nosso módulo trelloservice está configurado para solicitar recursivamente a autorização do usuário até que seja aprovado. Estaremos modificando isso para que o programa seja encerrado se a autorização não for concedida e solicitaremos que o usuário execute o comando config access . Isso garantirá que nosso programa seja mais limpo e descritivo em termos de instruções.


Para colocar isso em palavras, modificaremos estas funções:


  • __init__
  • __load_oauth_token_env_var
  • authorize
  • is_authorized


Começando com a função __init__ , inicializaremos um cliente vazio em vez de tratar da configuração do cliente aqui.

 # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None


Canto do Desafio 💡Você pode modificar nossa função __load_oauth_token_env_var para que ela não solicite recursivamente a autorização do usuário? Dica: Uma função recursiva é uma função que chama a si mesma.


Passando para as funções auxiliares authorize e is_authorized , a ideia é que authorize executará a lógica de negócios de configuração do cliente utilizando a função __load_oauth_token_env_var enquanto a função is_authorized apenas retorna um valor booleano informando se a autorização foi concedida.

 # 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


Entenda que a diferença entre __load_oauth_token_env_var e authorize é que __load_oauth_token_env_var é uma função interna que serve para armazenar o token de autorização como uma variável de ambiente, enquanto authorize sendo a função pública, ela tenta recuperar todas as credenciais necessárias e inicializar um cliente Trello.


Canto do Desafio 💡Observe como nossa função authorize retorna um tipo de dados AuthorizeResponse . Você pode implementar um modelo que possua o atributo status_code ? Consulte a Parte 1 de Como criar um programa Python CLI para gerenciamento de quadro Trello (dica: veja como criamos modelos)


Por último, vamos instanciar um objeto TrelloService singleton na parte inferior do módulo. Sinta-se à vontade para consultar este patch para ver a aparência do código completo: trello-cli-kit

 # trellocli/trelloservice.py trellojob = TrelloService()


Finalmente, queremos inicializar algumas exceções personalizadas para serem compartilhadas em todo o programa. Isso é diferente dos ERRORS definidos em nosso inicializador, pois essas exceções são subclasses de BaseException e atuam como exceções típicas definidas pelo usuário, enquanto os ERRORS servem mais como valores constantes começando em 0.


Vamos manter nossas exceções ao mínimo e seguir alguns dos casos de uso comuns, principalmente:


  • Erro de leitura: gerado quando há um erro ao ler do Trello
  • Erro de gravação: gerado quando há um erro ao escrever no Trello
  • Erro de autorização: gerado quando a autorização não é concedida para o Trello
  • Erro de entrada do usuário inválido: gerado quando a entrada CLI do usuário não é reconhecida


 # trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass


Testes unitários

Conforme mencionado na Parte I, não cobriremos extensivamente os Testes de Unidade neste tutorial, então vamos trabalhar apenas com os elementos necessários:


  • Teste para configurar o acesso
  • Teste para configurar o quadro trello
  • Teste para criar um novo cartão Trello
  • Teste para exibir detalhes do quadro Trello
  • Teste para exibir detalhes do quadro Trello (visualização detalhada)


A ideia é simular um interpretador de linha de comando, como um shell para testar os resultados esperados. O que é ótimo no módulo Typer é que ele vem com seu próprio objeto runner . Quanto à execução dos testes, iremos emparelhá-los com o módulo pytest . Para obter mais informações, consulte os documentos oficiais da Typer .


Vamos trabalhar juntos no primeiro teste, ou seja, configurar o acesso. Entenda que estamos testando se a função é executada corretamente. Para fazer isso, verificaremos a resposta do sistema e se o código de saída é success , também conhecido como 0. Aqui está um ótimo artigo da RedHat sobre o que são códigos de saída e como o sistema os usa para comunicar processos .

 # 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


Canto do Desafio 💡Agora que você entendeu a essência, pode implementar outros casos de teste por conta própria? (Dica: você também deve considerar testar casos de falha)


Logíca de negócios


Módulo CLI Principal

Entenda que este será nosso módulo cli principal - para todos os grupos de comandos (config, create), sua lógica de negócios será armazenada em seu próprio arquivo separado para melhor legibilidade.


Neste módulo, armazenaremos nosso comando list . Indo mais fundo no comando, sabemos que queremos implementar as seguintes opções:


  • board_name: obrigatório se config board não tiver sido definida anteriormente
  • detalhado: exibir em uma visão detalhada


Começando com a opção board_name requerida, existem algumas maneiras de conseguir isso, sendo uma delas usando a função de retorno de chamada (para mais informações, aqui estão os documentos oficiais ) ou simplesmente usando uma variável de ambiente padrão. No entanto, para nosso caso de uso, vamos simplificar, gerando nossa exceção personalizada InvalidUserInputError se as condições não forem atendidas.


Para construir o comando, vamos começar definindo as opções. No Typer, conforme mencionado em seus documentos oficiais , os principais ingredientes para definir uma opção seriam:


  • Tipo de dados
  • Texto auxiliar
  • Valor padrão


Por exemplo, para criar a opção detailed com as seguintes condições:


  • Tipo de dados: bool
  • Texto auxiliar: “Ativar visualização detalhada”
  • Valor padrão: Nenhum


Nosso código ficaria assim:

 detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None


No geral, para definir o comando list com as opções necessárias, trataremos list como uma função Python e suas opções como parâmetros necessários.

 # 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


Observe que estamos adicionando o comando à instância do app inicializada na parte superior do arquivo. Sinta-se à vontade para navegar pela base de código oficial do Typer para modificar as opções ao seu gosto.


Quanto ao fluxo de trabalho do comando, vamos fazer algo assim:


  • Verifique a autorização
  • Configure para usar a placa apropriada (verifique se a opção board_name foi fornecida)
  • Configure o quadro Trello para ser lido
  • Recuperar dados apropriados do cartão Trello e categorizar com base na lista do Trello
  • Exibir dados (verifique se a opção detailed foi selecionada)


Algumas coisas a serem observadas…


  • Queremos gerar exceções quando o trellojob produzir um código de status diferente de SUCCESS . Usando blocos try-catch , podemos evitar que nosso programa sofra falhas fatais.
  • Ao configurar o quadro apropriado para uso, tentaremos configurar o quadro do Trello para uso com base no board_id recuperado. Assim, queremos cobrir os seguintes casos de uso
    • Recuperando o board_id se o board_name foi fornecido explicitamente, verificando uma correspondência usando a função get_all_boards no trellojob
    • Recuperando o board_id armazenado como uma variável de ambiente se a opção board_name não tiver sido usada
  • Os dados que exibiremos serão formatados usando a funcionalidade Table do pacote rich . Para obter mais informações sobre rich , consulte seus documentos oficiais
    • Detalhado: exibe um resumo do número de listas do Trello, número de cartões e rótulos definidos. Para cada lista do Trello, exiba todos os cartões e seus nomes, descrições e rótulos associados correspondentes
    • Não detalhado: exibe um resumo do número de listas do Trello, número de cartões e rótulos definidos


Juntando tudo, obtemos algo como segue. Isenção de responsabilidade: pode haver algumas funções faltando no TrelloService que ainda precisamos implementar. Consulte este patch se precisar de ajuda para implementá-los: 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...")


Para ver nosso software em ação, basta executar python -m trellocli --help no terminal. Por padrão, o módulo Typer preencherá a saída do comando --help por conta própria. E observe como podemos chamar trellocli como o nome do pacote - lembra como isso foi definido anteriormente em nosso pyproject.toml ?


Vamos avançar um pouco e inicializar os grupos de comandos create e config também. Para fazer isso, simplesmente usaremos a função add_typer em nosso objeto app . A ideia é que o grupo de comandos tenha seu próprio objeto app , e vamos apenas adicioná-lo ao app principal em cli.py , junto com o nome do grupo de comandos e o texto auxiliar. Deveria ser algo assim

 # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations")


Canto do Desafio 💡Você poderia importar o grupo de comandos create sozinho? Sinta-se à vontade para consultar este patch para obter ajuda: trello-cli-kit


Subcomandos

Para configurar um grupo de comandos para create , armazenaremos seus respectivos comandos em seu próprio módulo. A configuração é semelhante à do cli.py com a necessidade de instanciar um objeto Typer. Quanto aos comandos, também gostaríamos de aderir à necessidade de usar exceções personalizadas. Um tópico adicional que queremos abordar é quando o usuário pressiona Ctrl + C , ou em outras palavras, interrompe o processo. A razão pela qual não cobrimos isso em nosso comando list é porque a diferença aqui é que o grupo de comandos config consiste em comandos interativos. A principal diferença entre comandos interativos é que eles exigem interação contínua do usuário. Claro, digamos que nosso comando direto leva muito tempo para ser executado. Também é uma prática recomendada lidar com possíveis interrupções do teclado.


Começando com o comando access , finalmente usaremos a função authorize criada em nosso TrelloService . Como a função authorize cuida da configuração sozinha, só teremos que verificar a execução do processo.

 # 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...")


Quanto ao comando board , utilizaremos vários módulos para fornecer uma boa experiência ao usuário, incluindo o Simple Terminal Menu para exibir uma GUI do terminal para interação do usuário. A ideia principal é a seguinte:


  • Verifique a autorização
  • Recuperar todos os painéis do Trello da conta do usuário
  • Exibir um menu de terminal de seleção única de placas Trello
  • Defina o ID do quadro Trello selecionado como uma variável de ambiente


 # 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...")


Finalmente, estamos avançando para o principal requisito funcional do nosso software: adicionar um novo cartão a uma lista no quadro do Trello. Estaremos usando as mesmas etapas do nosso comando list até recuperar os dados do quadro.


Além disso, solicitaremos interativamente a entrada do usuário para configurar corretamente o novo cartão:


  • Lista do Trello a ser adicionada a: Seleção única
  • Nome do cartão: Texto
  • [Opcional] Descrição do cartão: Texto
  • [Opcional] Rótulos: seleção múltipla
  • Confirmação: s/N


Para todos os prompts que exigem que o usuário selecione em uma lista, usaremos o pacote Simple Terminal Menu como antes. Quanto a outros prompts e itens diversos, como a necessidade de entrada de texto ou a confirmação do usuário, usaremos simplesmente o pacote rich . Também é importante observar que temos que lidar adequadamente com os valores opcionais:


  • Os usuários podem ignorar o fornecimento de uma descrição
  • Os usuários podem fornecer uma seleção vazia para rótulos


 # 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...")


Canto do Desafio 💡Você pode exibir uma barra de progresso para o processo add ? Dica: dê uma olhada no uso do recurso de status de rich


Distribuição de Pacotes

Aí vem a parte divertida - distribuir oficialmente nosso software no PyPI. Estaremos seguindo este pipeline para fazer isso:


  • Configurar metadados + atualizar README
  • Carregar para testar PyPI
  • Configurar ações do GitHub
  • Enviar código para Tag v1.0.0
  • Distribuir código para PyPI 🎉


Para uma explicação detalhada, confira este ótimo tutorial sobre empacotamento Python de Ramit Mittal.


Configuração de metadados

O último detalhe que precisamos para nosso pyproject.toml é especificar qual módulo armazena o pacote em si. No nosso caso, será trellocli . Aqui estão os metadados a serem adicionados:

 # pyproject.toml [tool.setuptools] packages = ["trellocli"]


Quanto ao nosso README.md , é uma ótima prática fornecer algum tipo de guia, seja ele diretrizes de uso ou como começar. Se você incluiu imagens em seu README.md , você deve usar seu URL absoluto, que geralmente tem o seguinte formato

 https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image>


TestePyPI

Estaremos usando as ferramentas build e twine para construir e publicar nosso pacote. Execute o seguinte comando em seu terminal para criar um arquivo fonte e uma roda para seu pacote:

 python -m build


Certifique-se de que você já tenha uma conta configurada no TestPyPI e execute o seguinte comando

 twine upload -r testpypi dist/*


Você será solicitado a digitar seu nome de usuário e senha. Por ter a autenticação de dois fatores habilitada, você precisará usar um token de API (para obter mais informações sobre como adquirir um token de API TestPyPI: link para a documentação ). Basta colocar os seguintes valores:


  • nome de usuário: símbolo
  • senha: <seu token TestPyPI>


Depois de concluído, você poderá acessar TestPyPI para verificar seu pacote recém-distribuído!


Configuração do GitHub

O objetivo é utilizar o GitHub como um meio de atualizar continuamente novas versões do seu pacote com base em tags.


Primeiro, vá até a guia Actions no fluxo de trabalho do GitHub e selecione um novo fluxo de trabalho. Usaremos o fluxo de trabalho Publish Python Package que foi criado pelo GitHub Actions. Observe como o fluxo de trabalho requer a leitura dos segredos do repositório? Certifique-se de ter armazenado seu token PyPI com o nome especificado (adquirir um token de API PyPI é semelhante ao TestPyPI).


Depois que o fluxo de trabalho for criado, enviaremos nosso código para a tag v1.0.0. Para obter mais informações sobre a sintaxe de nomenclatura de versões, aqui está uma ótima explicação do Py-Pkgs: link para a documentação


Basta executar os comandos usuais pull , add e commit . Em seguida, crie uma tag para seu commit mais recente executando o seguinte comando (para mais informações sobre tags: link para documentação )

 git tag <tagname> HEAD


Por fim, envie sua nova tag para o repositório remoto

 git push <remote name> <tag name>


Aqui está um ótimo artigo de Karol Horosin sobre Integração de CI/CD com seu pacote Python se você quiser saber mais. Mas, por enquanto, relaxe e aproveite sua última conquista 🎉. Sinta-se à vontade para assistir a mágica acontecer como um fluxo de trabalho do GitHub Actions enquanto ele distribui seu pacote para o PyPI.


Embrulhar

Este foi longo 😓. Através deste tutorial, você aprendeu a transformar seu software em um programa CLI usando o módulo Typer e distribuir seu pacote para PyPI. Para se aprofundar, você aprendeu a definir comandos e grupos de comandos, desenvolver uma sessão CLI interativa e brincar com cenários CLI comuns, como interrupção do teclado.


Você tem sido um mago absoluto por resistir a tudo isso. Você não gostaria de se juntar a mim na Parte 3, onde implementamos as funcionalidades opcionais?