Olá, vamos jogar! Escolha seu lutador (pedra, papel, tesoura): pedra
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.
Por exemplo, quando queremos definir a propriedade project
na seção principal, executamos gcloud config set project <PROJECT_ID>
Argumento | Contente |
---|---|
Arg 0 | gcloud |
Arg 1 | configuração |
… | … |
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.
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
)
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>
.
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.
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.
Requisitos funcionais
Requisitos não Funcionais
Requisitos Opcionais
Ps Não se preocupe com as duas últimas colunas, aprenderemos sobre isso mais tarde…
Testes de unidade
Trello
CLI
Utilitários (Diversos)
Parte 1
py-trello
Parte 2
Parte 3
O computador escolheu a tesoura! Vamos ver quem ganha essa batalha...
trellocli/ __init__.py __main__.py models.py cli.py trelloservice.py tests/ test_cli.py test_trelloservice.py README.md pyproject.toml .env .gitignore
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
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:
<!--- README.md --> # Overview # Getting Started # Usage # Architecture ## Data Flow ## Tech Stack # Running Tests # Next Steps # References
# 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" = ""
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 . 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 . 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:
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
: . 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…
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.
# 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 consultar 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
( ), 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
# 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 consultar 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
# 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 consultar para ver como é minha implementação
# 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 )
Parabéns! Você ganhou. Jogar de novo (s/N)?