Isenção de responsabilidade: este tutorial pressupõe que os leitores tenham um conhecimento básico de Python, APIs, Git e testes de unidade.
Eu encontrei vários softwares CLI com as animações mais legais, e isso me fez pensar - eu poderia atualizar meu projeto escolar 'minimalista' pedra-papel-tesoura?
Olá, vamos jogar! Escolha seu lutador (pedra, papel, tesoura): pedra
Conforme declarado na Wikipedia, “uma interface de linha de comando (CLI) é um meio de interagir com um dispositivo ou programa de computador com comandos de um usuário ou cliente e respostas do dispositivo ou programa, na forma de linhas de texto”.
Em outras palavras, um programa CLI é um programa pelo qual o usuário usa a linha de comando para interagir com o programa fornecendo instruções para execução.
Muitos softwares do dia-a-dia são agrupados como um programa CLI. Pegue o editor de texto vim
, por exemplo - uma ferramenta fornecida com qualquer sistema UNIX que pode ser ativada simplesmente executando vim <FILE>
no terminal.
Com relação à CLI do Google Cloud , vamos nos aprofundar na anatomia de um programa CLI.
Argumentos (Parâmetros) são itens de informação fornecidos a um programa. Muitas vezes, são chamados de argumentos posicionais porque são identificados por sua posição.
Por exemplo, quando queremos definir a propriedade project
na seção principal, executamos gcloud config set project <PROJECT_ID>
Notavelmente, podemos traduzir isso em
Argumento | Contente |
---|---|
Arg 0 | gcloud |
Arg 1 | configuração |
… | … |
Os comandos são uma matriz de argumentos que fornecem instruções ao computador.
Com base no exemplo anterior, definimos a propriedade project
na seção principal executando gcloud config set project <PROJECT_ID>
Em outras palavras, set
é um comando.
Normalmente, os comandos são necessários, mas podemos fazer exceções. Com base no caso de uso do programa, podemos definir comandos opcionais.
Voltando ao comando gcloud config
, conforme declarado em sua documentação oficial, gcloud config
é um grupo de comandos que permite modificar propriedades. O uso é assim:
gcloud config GROUP | COMMAND [GCLOUD_WIDE_FLAG … ]
pelo qual COMMAND pode ser set
, list
, e assim por diante… (Observe que GROUP é config
)
Opções são tipos documentados de parâmetros que modificam o comportamento de um comando. Eles são pares chave-valor indicados por '-' ou '--'.
Voltando ao uso do grupo de comandos gcloud config
, a(s) opção(ões), neste caso, é GCLOUD_WIDE_FLAG
.
Por exemplo, digamos que queremos exibir o uso e a descrição detalhados do comando, executamos gcloud config set –help
. Em outras palavras, --help
é a opção.
Outro exemplo é quando queremos definir a propriedade de zona na seção de computação de um projeto específico, executamos gcloud config set compute <ZONE_NAME> –project=<PROJECT_ID>
. Em outras palavras, --project
é uma opção que contém o valor <PROJECT_ID>
.
Também é importante observar que suas posições geralmente não importam.
As opções, como seu nome, geralmente são opcionais, mas também podem ser personalizadas para serem obrigatórias.
Por exemplo, quando queremos criar um cluster dataproc, executamos gcloud dataproc clusters create <CLUSTER_NAME> –region=<REGION>
. E conforme declarado em sua documentação de uso:
gcloud dataproc clusters create (CLUSTER: –region=REGION)
O sinalizador --region
é obrigatório se não tiver sido configurado anteriormente.
As opções curtas começam com -
seguidas por um único caractere alfanumérico, enquanto as opções longas começam com --
seguidas por vários caracteres. Pense nas opções curtas como atalhos quando o usuário tiver certeza do que deseja, enquanto as opções longas são mais legíveis.
Você escolheu a pedra! O computador agora fará sua seleção.
Então eu menti… Não tentaremos atualizar o programa CLI pedra-papel-tesoura básico.
Em vez disso, vamos dar uma olhada em um cenário do mundo real:
Sua equipe usa o Trello para acompanhar os problemas e o progresso do projeto. Sua equipe está procurando uma maneira mais simplificada de interagir com o quadro - algo semelhante a criar um novo repositório GitHub por meio do terminal. A equipe recorreu a você para criar um programa CLI com esse requisito básico de poder adicionar um novo cartão à coluna 'Tarefas' do quadro.
Com base no requisito mencionado, vamos esboçar nosso programa CLI definindo seus requisitos:
Requisitos funcionais
Requisitos não Funcionais
Requisitos Opcionais
Com base no exposto, podemos formalizar os comandos e opções do nosso programa CLI como tal:
Ps Não se preocupe com as duas últimas colunas, aprenderemos sobre isso mais tarde…
Quanto à nossa pilha de tecnologia, vamos nos ater a isso:
Testes de unidade
Trello
CLI
Utilitários (Diversos)
Estaremos abordando este projeto em partes e aqui está um trecho do que você pode esperar:
Parte 1
py-trello
Parte 2
Parte 3
O computador escolheu a tesoura! Vamos ver quem ganha essa batalha...
O objetivo é distribuir o programa CLI como um pacote em PyPI . Assim, tal configuração é necessária:
trellocli/ __init__.py __main__.py models.py cli.py trelloservice.py tests/ test_cli.py test_trelloservice.py README.md pyproject.toml .env .gitignore
Aqui está um mergulho profundo em cada arquivo e/ou diretório:
trellocli
: atua como o nome do pacote a ser usado pelos usuários, por exemplo, pip install trellocli
__init__.py
: representa a raiz do pacote, conforma a pasta como um pacote Python__main__.py
: define o ponto de entrada e permite que os usuários executem módulos sem especificar o caminho do arquivo usando o sinalizador -m
, por exemplo, python -m <module_name>
para substituir python -m <parent_folder>/<module_name>.py
models.py
: armazena classes usadas globalmente, por exemplo, modelos com os quais se espera que as respostas da API estejam em conformidadecli.py
: armazena a lógica de negócios para comandos e opções da CLItrelloservice.py
: armazena a lógica de negócios para interagir com py-trello
tests
: armazena testes de unidade para o programatest_cli.py
: armazena testes de unidade para a implementação da CLItest_trelloservice.py
: armazena testes de unidade para a interação com py-trello
README.md
: armazena a documentação do programapyproject.toml
: armazena as configurações e requisitos do pacote.env
: armazena variáveis de ambiente.gitignore
: especifica os arquivos a serem ignorados (não rastreados) durante o controle de versão
Para obter uma explicação mais detalhada sobre a publicação de pacotes Python, confira este ótimo artigo: How to Publish an Open-Source Python Package to PyPI by Geir Arne Hjelle
Antes de começarmos, vamos nos basear na configuração do pacote.
Começando com o arquivo __init__.py
em nosso pacote, que seria onde as constantes e variáveis do pacote são armazenadas, como nome e versão do aplicativo. No nosso caso, queremos inicializar o seguinte:
# trellocli/__init__.py __app_name__ = "trellocli" __version__ = "0.1.0" ( SUCCESS, TRELLO_WRITE_ERROR, TRELLO_READ_ERROR ) = range(3) ERRORS = { TRELLO_WRITE_ERROR: "Error when writing to Trello", TRELLO_READ_ERROR: "Error when reading from Trello" }
Passando para o arquivo __main__.py
, o fluxo principal do seu programa deve ser armazenado aqui. No nosso caso, armazenaremos o ponto de entrada do programa CLI, assumindo que haverá uma função chamável em cli.py
.
# trellocli/__main__.py from trellocli import cli def main(): # we'll modify this later - after the implementation of `cli.py` pass if __name__ == "__main__": main()
Agora que o pacote foi configurado, vamos dar uma olhada na atualização do nosso arquivo README.md
(documentação principal). Não existe uma estrutura específica que devemos seguir, mas um bom README consistiria no seguinte:
Outro ótimo post para ler se você quiser se aprofundar: Como escrever um bom README por merlos
Aqui está como eu gostaria de estruturar o README para este projeto
<!--- README.md --> # Overview # Getting Started # Usage # Architecture ## Data Flow ## Tech Stack # Running Tests # Next Steps # References
Vamos deixar o esqueleto como está por enquanto - voltaremos a isso mais tarde.
Seguindo em frente, vamos configurar os metadados do nosso pacote com base na documentação oficial
# pyproject.toml [project] name = "trellocli_<YOUR_USERNAME>" version = "0.1.0" authors = [ { name = "<YOUR_NAME>", email = "<YOUR_EMAIL>" } ] description = "Program to modify your Trello boards from your computer's command line" readme = "README.md" requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] dependencies = [] [project.urls] "Homepage" = ""
Observe como existem espaços reservados que você deve modificar, por exemplo, seu nome de usuário, seu nome…
Em outra observação, deixaremos o URL da página inicial vazio por enquanto. Faremos alterações depois de publicá-lo no GitHub. Também deixaremos a parte de dependências vazia por enquanto e adicionaremos à medida que avançamos.
O próximo na lista seria nosso arquivo .env
, onde armazenamos nossas variáveis de ambiente, como chaves e segredos de API. É importante observar que esse arquivo não deve ser rastreado pelo Git, pois contém informações confidenciais.
No nosso caso, armazenaremos nossas credenciais do Trello aqui. Para criar um Power-Up no Trello, siga este guia . Mais especificamente, com base no uso do py-trello
, como pretendemos usar OAuth para nosso aplicativo, precisaremos do seguinte para interagir com o Trello:
Depois de recuperar sua chave e segredo de API, armazene-os no arquivo .env
como tal
# .env TRELLO_API_KEY=<your_api_key> TRELLO_API_SECRET=<your_api_secret>
Por último, mas não menos importante, vamos usar o template Python .gitignore
que pode ser encontrado aqui . Observe que isso é crucial para garantir que nosso arquivo .env
nunca seja rastreado - se em algum momento, nosso arquivo .env
foi rastreado, mesmo se removermos o arquivo em etapas posteriores, o dano está feito e agentes mal-intencionados podem rastrear o anterior patches para informações confidenciais.
Agora que a configuração está concluída, vamos enviar nossas alterações para o GitHub. Dependendo dos metadados especificados em pyproject.toml
, lembre-se de atualizar sua LICENÇA e o URL da página inicial de acordo. Para referência sobre como escrever commits melhores: Write Better Commits, Build Better Projects por Victoria Dye
Outros passos notáveis:
Antes de começarmos a escrever nossos testes, é importante observar que, como estamos trabalhando com uma API, implementaremos testes simulados para poder testar nosso programa sem o risco de indisponibilidade da API. Aqui está outro ótimo artigo sobre testes simulados do Real Python: Mocking External APIs in Python
Com base nos requisitos funcionais, nossa principal preocupação é permitir que os usuários adicionem um novo cartão. Referenciando o método em py-trello
: add_card . Para poder fazer isso, devemos chamar o método add_card
da classe List
, que pode ser recuperado da função get_list
da classe Board
, que pode ser recuperado…
Você entendeu a essência - precisaremos de muitos métodos auxiliares para chegar ao nosso destino final, vamos colocar em palavras:
Também é importante observar que, ao escrever testes de unidade, queremos que nossos testes sejam o mais extensos possível - ele lida bem com os erros? Abrange todos os aspectos do nosso programa?
No entanto, apenas para fins deste tutorial, simplificaremos as coisas verificando apenas os casos de sucesso.
Antes de mergulhar no código, vamos modificar nosso arquivo pyproject.toml
para incluir as dependências necessárias para escrever/executar testes de unidade.
# pyproject.toml [project] dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1" ]
Em seguida, vamos ativar nosso virtualenv e executar pip install .
para instalar as dependências.
Feito isso, vamos finalmente escrever alguns testes. Em geral, nossos testes devem incluir uma resposta simulada a ser retornada, um patch para a função que estamos tentando testar corrigindo o valor de retorno com a resposta simulada e, finalmente, uma chamada para a função. Um teste de amostra para recuperar os tokens de acesso do usuário seria o seguinte:
# tests/test_trelloservice.py # module imports from trellocli import SUCCESS from trellocli.trelloservice import TrelloService from trellocli.models import * # dependencies imports # misc imports def test_get_access_token(mocker): """Test to check success retrieval of user's access token""" mock_res = GetOAuthTokenResponse( token="test", token_secret="test", status_code=SUCCESS ) mocker.patch( "trellocli.trelloservice.TrelloService.get_user_oauth_token", return_value=mock_res ) trellojob = TrelloService() res = trellojob.get_user_oauth_token() assert res.status_code == SUCCESS
Observe em meu código de amostra que GetOAuthTokenResponse
é um modelo que ainda não foi definido em models.py
. Ele fornece estrutura para escrever um código mais limpo, veremos isso em ação mais tarde.
Para executar nossos testes, basta executar python -m pytest
. Observe como nossos testes falharão, mas tudo bem - funcionará no final.
Cantinho do Desafio 💡 Você pode tentar escrever mais testes sozinho? Sinta-se à vontade para consultareste patch para ver como são meus testes
Por enquanto, vamos construir nosso trelloservice
. Começando com a adição de uma nova dependência, que é o wrapper py-trello
.
# pyproject.toml dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1", "py-trello==0.19.0" ]
Mais uma vez, execute pip install .
para instalar as dependências.
Agora, vamos começar construindo nossos modelos - para regular as respostas que esperamos no trelloservice
. Para esta parte, é melhor consultar nossos testes de unidade e o código-fonte py-trello
para entender o tipo de valor de retorno que podemos esperar.
Por exemplo, digamos que queremos recuperar o token de acesso do usuário, referindo-se à função create_oauth_token
do py-trello
( código-fonte ), sabemos que devemos esperar que o valor de retorno seja algo como isto
# trellocli/models.py # module imports # dependencies imports # misc imports from typing import NamedTuple class GetOAuthTokenResponse(NamedTuple): token: str token_secret: str status_code: int
Por outro lado, esteja ciente das convenções de nomenclatura conflitantes. Por exemplo, o módulo py-trello
tem uma classe chamada List
. Uma solução para isso seria fornecer um alias durante a importação.
# trellocli/models.py # dependencies imports from trello import List as Trellolist
Sinta-se à vontade para também usar esta oportunidade para adaptar os modelos às necessidades do seu programa. Por exemplo, digamos que você precise apenas de um atributo do valor de retorno, você pode refatorar seu modelo para esperar extrair o referido valor do valor de retorno em vez de armazená-lo como um todo.
# trellocli/models.py class GetBoardName(NamedTuple): """Model to store board id Attributes id (str): Extracted board id from Board value type """ id: str
Cantinho do Desafio 💡 Você pode tentar escrever mais modelos sozinho? Sinta-se à vontade para consultareste patch para ver como meus modelos se parecem
Modelos desativados, vamos começar oficialmente a codificar o trelloservice
. Novamente, devemos nos referir aos testes de unidade que criamos - diga que a lista atual de testes não fornece cobertura total para o serviço, sempre retorne e adicione mais testes quando necessário.
Como de costume, inclua todas as instruções de importação na parte superior. Em seguida, crie a classe TrelloService
e os métodos de espaço reservado conforme o esperado. A ideia é inicializar uma instância compartilhada do serviço em cli.py
e chamar seus métodos de acordo. Além disso, visamos a escalabilidade, portanto, a necessidade de uma ampla cobertura.
# trellocli/trelloservice.py # module imports from trellocli import TRELLO_READ_ERROR, TRELLO_WRITE_ERROR, SUCCESS from trellocli.models import * # dependencies imports from trello import TrelloClient # misc imports class TrelloService: """Class to implement the business logic needed to interact with Trello""" def __init__(self) -> None: pass def get_user_oauth_token() -> GetOAuthTokenResponse: pass def get_all_boards() -> GetAllBoardsResponse: pass def get_board() -> GetBoardResponse: pass def get_all_lists() -> GetAllListsResponse: pass def get_list() -> GetListResponse: pass def get_all_labels() -> GetAllLabelsResponse: pass def get_label() -> GetLabelResponse: pass def add_card() -> AddCardResponse: pass
Observe como, desta vez, quando executarmos nossos testes, nossos testes serão aprovados. Na verdade, isso nos ajudará a garantir que estamos no caminho certo. O fluxo de trabalho deve ser estender nossas funções, executar nossos testes, verificar aprovação/reprovação e refatorar de acordo.
Vamos começar com a função __init__
. A ideia é chamar a função get_user_oauth_token
aqui e inicializar o TrelloClient
. Novamente, enfatizando a necessidade de armazenar essas informações confidenciais apenas no arquivo .env
, usaremos a dependência python-dotenv
para recuperar informações confidenciais. Depois de modificar nosso arquivo pyproject.toml
adequadamente, vamos começar a implementar as etapas de autorização.
# trellocli/trelloservice.py class TrelloService: """Class to implement the business logic needed to interact with Trello""" def __init__(self) -> None: self.__load_oauth_token_env_var() self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) def __load_oauth_token_env_var(self) -> None: """Private method to store user's oauth token as an environment variable""" load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): res = self.get_user_oauth_token() if res.status_code == SUCCESS: dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_OAUTH_TOKEN", value_to_set=res.token ) else: print("User denied access.") self.__load_oauth_token_env_var() def get_user_oauth_token(self) -> GetOAuthTokenResponse: """Helper method to retrieve user's oauth token Returns GetOAuthTokenResponse: user's oauth token """ try: res = create_oauth_token() return GetOAuthTokenResponse( token=res["oauth_token"], token_secret=res["oauth_token_secret"], status_code=SUCCESS ) except: return GetOAuthTokenResponse( token="", token_secret="", status_code=TRELLO_AUTHORIZATION_ERROR )
Nesta implementação, criamos um método auxiliar para lidar com quaisquer erros previsíveis, por exemplo, quando o usuário clica em Deny
durante a autorização. Além disso, ele está configurado para solicitar recursivamente a autorização do usuário até que uma resposta válida seja retornada, porque o fato é que não podemos continuar a menos que o usuário autorize nosso aplicativo a acessar os dados de sua conta.
Canto do desafio 💡 Observe TRELLO_AUTHORIZATION_ERROR
? Você pode declarar este erro como uma constante de pacote? Consulte Configuração para obter mais informações
Agora que a parte de autorização está concluída, vamos passar para as funções auxiliares, começando com a recuperação dos quadros Trello do usuário.
# trellocli/trelloservice.py def get_all_boards(self) -> GetAllBoardsResponse: """Method to list all boards from user's account Returns GetAllBoardsResponse: array of user's trello boards """ try: res = self.__client.list_boards() return GetAllBoardsResponse( res=res, status_code=SUCCESS ) except: return GetAllBoardsResponse( res=[], status_code=TRELLO_READ_ERROR ) def get_board(self, board_id: str) -> GetBoardResponse: """Method to retrieve board Required Args board_id (str): board id Returns GetBoardResponse: trello board """ try: res = self.__client.get_board(board_id=board_id) return GetBoardResponse( res=res, status_code=SUCCESS ) except: return GetBoardResponse( res=None, status_code=TRELLO_READ_ERROR )
Quanto à recuperação das listas (colunas), teremos que verificar a classe Board
do py-trello
, ou seja, devemos aceitar um novo parâmetro do tipo de valor Board
.
# trellocli/trelloservice.py def get_all_lists(self, board: Board) -> GetAllListsResponse: """Method to list all lists (columns) from the trello board Required Args board (Board): trello board Returns GetAllListsResponse: array of trello lists """ try: res = board.all_lists() return GetAllListsResponse( res=res, status_code=SUCCESS ) except: return GetAllListsResponse( res=[], status_code=TRELLO_READ_ERROR ) def get_list(self, board: Board, list_id: str) -> GetListResponse: """Method to retrieve list (column) from the trello board Required Args board (Board): trello board list_id (str): list id Returns GetListResponse: trello list """ try: res = board.get_list(list_id=list_id) return GetListResponse( res=res, status_code=SUCCESS ) except: return GetListResponse( res=None, status_code=TRELLO_READ_ERROR )
Canto do desafio 💡 Você poderia implementar as funções get_all_labels
e get_label
por conta própria? Revise a classe Board
do py-trello
. Sinta-se à vontade para consultareste patch para ver como é minha implementação
Por último, mas não menos importante, finalmente alcançamos o que almejamos o tempo todo - adicionar um novo cartão. Lembre-se de que não usaremos todas as funções declaradas anteriormente aqui - o objetivo das funções auxiliares é aumentar a escalabilidade.
# trellocli/trelloservice.py def add_card( self, col: Trellolist, name: str, desc: str = "", labels: List[Label] = [] ) -> AddCardResponse: """Method to add a new card to a list (column) on the trello board Required Args col (Trellolist): trello list name (str): card name Optional Args desc (str): card description labels (List[Label]): list of labels to be added to the card Returns AddCardResponse: newly-added card """ try: # create new card new_card = col.add_card(name=name) # add optional description if desc: new_card.set_description(description=desc) # add optional labels if labels: for label in labels: new_card.add_label(label=label) return AddCardResponse( res=new_card, status_code=SUCCESS ) except: return AddCardResponse( res=new_card, status_code=TRELLO_WRITE_ERROR )
🎉 Agora que está pronto, lembre-se de atualizar seu README de acordo e enviar seu código para o GitHub.
Parabéns! Você ganhou. Jogar de novo (s/N)?
Obrigado pela paciência :) Por meio deste tutorial, você aprendeu com sucesso a implementar a simulação ao escrever testes de unidade, estruturar modelos para coesão, ler o código-fonte para encontrar as principais funcionalidades e implementar a lógica de negócios usando um wrapper de terceiros.
Fique de olho na Parte 2, onde faremos um mergulho profundo na implementação do próprio programa CLI.
Enquanto isso, vamos manter contato 👀
Código-fonte do GitHub: https://github.com/elainechan01/trellocli