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 , criamos com sucesso a lógica de negócios para interagir com o Trello SDK. Como criar um programa Python CLI para gerenciamento do Trello Board (Parte 1) Aqui está uma rápida recapitulação da arquitetura do nosso programa CLI: 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 . Desta vez, queremos implementar uma pasta com módulos para diferentes funcionalidades, nomeadamente: trelloservice cli 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 , iremos armazená-lo no arquivo CLI principal, pois não pertence a nenhum grupo de comandos. list 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 ? Isso será usado para armazenar ativos relacionados para nosso , embora exista uma pasta recém-implementada em , vamos usá-la para armazenar módulos a serem usados em todo o software. assets README shared trellocli Configurar Vamos começar modificando nosso arquivo de ponto de entrada, . 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, , possui uma instância que podemos executar. __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() Avance rapidamente para nosso arquivo ; armazenaremos nossa instância aqui. A ideia é inicializar um objeto para ser compartilhado pelo software. cli.py app Typer # 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 para especificar nossos scripts de linha de comando. Aqui, forneceremos um nome para nosso pacote e definiremos o ponto de entrada. pyproject.toml # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main" Com base no exemplo acima, definimos como o nome do pacote e a função no script , que está armazenado no módulo , será executada durante o tempo de execução. trellocli main __main__ trellocli Agora que a parte CLI do nosso software está configurada, vamos modificar nosso módulo para melhor atender nosso programa CLI. Como você deve lembrar, nosso módulo 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 . Isso garantirá que nosso programa seja mais limpo e descritivo em termos de instruções. trelloservice trelloservice config access Para colocar isso em palavras, modificaremos estas funções: __init__ __load_oauth_token_env_var authorize is_authorized Começando com a função , inicializaremos um cliente vazio em vez de tratar da configuração do cliente aqui. __init__ # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None 💡Você pode modificar nossa função 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. Canto do Desafio __load_oauth_token_env_var Passando para as funções auxiliares e , a ideia é que executará a lógica de negócios de configuração do cliente utilizando a função enquanto a função apenas retorna um valor booleano informando se a autorização foi concedida. 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 Entenda que a diferença entre e é que é uma que serve para armazenar o token de autorização como uma variável de ambiente, enquanto sendo a função pública, ela tenta recuperar todas as credenciais necessárias e inicializar um cliente Trello. __load_oauth_token_env_var authorize __load_oauth_token_env_var função interna authorize 💡Observe como nossa função retorna um tipo de dados . Você pode implementar um modelo que possua o atributo ? Consulte a (dica: veja como criamos modelos) Canto do Desafio authorize AuthorizeResponse status_code Parte 1 de Como criar um programa Python CLI para gerenciamento de quadro Trello Por último, vamos instanciar um objeto singleton na parte inferior do módulo. Sinta-se à vontade para consultar este patch para ver a aparência do código completo: TrelloService 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 definidos em nosso inicializador, pois essas exceções são subclasses de e atuam como exceções típicas definidas pelo usuário, enquanto os servem mais como valores constantes começando em 0. ERRORS BaseException ERRORS 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 para testar os resultados esperados. O que é ótimo no módulo é que ele vem com seu próprio objeto . Quanto à execução dos testes, iremos emparelhá-los com o módulo . Para obter mais informações, consulte os . shell Typer runner pytest 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 é , também conhecido como 0. Aqui está um ótimo artigo da RedHat sobre . success 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 💡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) Canto do Desafio Logíca de negócios Módulo CLI Principal Entenda que este será nosso módulo 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. cli Neste módulo, armazenaremos nosso comando . Indo mais fundo no comando, sabemos que queremos implementar as seguintes opções: list board_name: obrigatório se não tiver sido definida anteriormente config board 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 ) ou simplesmente usando uma variável de ambiente padrão. No entanto, para nosso caso de uso, vamos simplificar, gerando nossa exceção personalizada se as condições não forem atendidas. documentos oficiais InvalidUserInputError Para construir o comando, vamos começar definindo as opções. No Typer, conforme mencionado em seus , os principais ingredientes para definir uma opção seriam: documentos oficiais Tipo de dados Texto auxiliar Valor padrão Por exemplo, para criar a opção com as seguintes condições: detailed 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 com as opções necessárias, trataremos como uma função Python e suas opções como parâmetros necessários. 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 Observe que estamos adicionando o comando à instância do inicializada na parte superior do arquivo. Sinta-se à vontade para navegar pela para modificar as opções ao seu gosto. app base de código oficial do Typer 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 foi fornecida) board_name 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 foi selecionada) detailed Algumas coisas a serem observadas… Queremos gerar exceções quando o trellojob produzir um código de status diferente de . Usando blocos , podemos evitar que nosso programa sofra falhas fatais. SUCCESS try-catch Ao configurar o quadro apropriado para uso, tentaremos configurar o quadro do Trello para uso com base no recuperado. Assim, queremos cobrir os seguintes casos de uso board_id Recuperando o se o foi fornecido explicitamente, verificando uma correspondência usando a função no trellojob board_id board_name get_all_boards Recuperando o armazenado como uma variável de ambiente se a opção não tiver sido usada board_id board_name Os dados que exibiremos serão formatados usando a funcionalidade do pacote . Para obter mais informações sobre , consulte seus Table rich rich 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 que ainda precisamos implementar. Consulte este patch se precisar de ajuda para implementá-los: 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...") Para ver nosso software em ação, basta executar no terminal. Por padrão, o módulo Typer preencherá a saída do comando por conta própria. E observe como podemos chamar como o nome do pacote - lembra como isso foi definido anteriormente em nosso ? python -m trellocli --help --help trellocli pyproject.toml Vamos avançar um pouco e inicializar os grupos de comandos e também. Para fazer isso, simplesmente usaremos a função em nosso objeto . A ideia é que o grupo de comandos tenha seu próprio objeto , e vamos apenas adicioná-lo ao principal em , junto com o nome do grupo de comandos e o texto auxiliar. Deveria ser algo assim 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") 💡Você poderia importar o grupo de comandos sozinho? Sinta-se à vontade para consultar este patch para obter ajuda: Canto do Desafio create trello-cli-kit Subcomandos Para configurar um grupo de comandos para , armazenaremos seus respectivos comandos em seu próprio módulo. A configuração é semelhante à do 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 , ou em outras palavras, interrompe o processo. A razão pela qual não cobrimos isso em nosso comando é porque a diferença aqui é que o grupo de comandos 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. create cli.py Ctrl + C list config Começando com o comando , finalmente usaremos a função criada em nosso . Como a função cuida da configuração sozinha, só teremos que verificar a execução do processo. 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...") Quanto ao comando , utilizaremos vários módulos para fornecer uma boa experiência ao usuário, incluindo o para exibir uma GUI do terminal para interação do usuário. A ideia principal é a seguinte: board Simple Terminal Menu 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 até recuperar os dados do quadro. list 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 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 . Também é importante observar que temos que lidar adequadamente com os valores opcionais: Simple Terminal Menu rich 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...") 💡Você pode exibir uma barra de progresso para o processo ? Dica: dê uma olhada no uso de Canto do Desafio add do recurso de status 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 é especificar qual módulo armazena o pacote em si. No nosso caso, será . Aqui estão os metadados a serem adicionados: pyproject.toml trellocli # pyproject.toml [tool.setuptools] packages = ["trellocli"] Quanto ao nosso , é uma ótima prática fornecer algum tipo de guia, seja ele diretrizes de uso ou como começar. Se você incluiu imagens em seu , você deve usar seu URL absoluto, que geralmente tem o seguinte formato README.md README.md https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image> TestePyPI Estaremos usando as ferramentas e para construir e publicar nosso pacote. Execute o seguinte comando em seu terminal para criar um arquivo fonte e uma roda para seu pacote: build twine 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: ). Basta colocar os seguintes valores: link para a documentação 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 no fluxo de trabalho do GitHub e selecione um novo fluxo de trabalho. Usaremos o fluxo de trabalho 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). Actions Publish Python Package 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 , e . Em seguida, crie uma tag para seu commit mais recente executando o seguinte comando (para mais informações sobre tags: ) pull add commit 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 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. Karol Horosin sobre Integração de CI/CD com seu pacote Python Embrulhar Este foi longo 😓. Através deste tutorial, você aprendeu a transformar seu software em um programa CLI usando o módulo 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. Typer 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?