Já estamos muito além do projeto escolar básico de pedra-papel-tesoura - vamos mergulhar direto nele.
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:
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 .
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:
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.
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:
# trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass
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:
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)
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:
config board
não tiver sido definida anteriormente
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:
Por exemplo, para criar a opção detailed
com as seguintes condições:
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:
board_name
foi fornecida)detailed
foi selecionada)
Algumas coisas a serem observadas…
SUCCESS
. Usando blocos try-catch
, podemos evitar que nosso programa sofra falhas fatais.board_id
recuperado. Assim, queremos cobrir os seguintes casos de usoboard_id
se o board_name
foi fornecido explicitamente, verificando uma correspondência usando a função get_all_boards
no trellojobboard_id
armazenado como uma variável de ambiente se a opção board_name
não tiver sido usadaTable
do pacote rich
. Para obter mais informações sobre rich
, consulte seus documentos oficiais
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
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:
# 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:
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:
# 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
Aí vem a parte divertida - distribuir oficialmente nosso software no PyPI. Estaremos seguindo este pipeline para fazer isso:
Para uma explicação detalhada, confira este ótimo tutorial sobre empacotamento Python de Ramit Mittal.
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>
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:
Depois de concluído, você poderá acessar TestPyPI para verificar seu pacote recém-distribuído!
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.
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?