¡Hola, vamos a jugar! Elige tu luchador (piedra, papel, tijera): roca
Gran parte del software del día a día está empaquetado como un programa CLI. Tome el editor de texto vim
, por ejemplo, una herramienta incluida con cualquier sistema UNIX que se puede activar simplemente ejecutando vim <FILE>
en la terminal.
Por ejemplo, cuando queremos establecer la propiedad project
en la sección central, ejecutamos gcloud config set project <PROJECT_ID>
Argumento | Contenido |
---|---|
argumento 0 | gcloud |
argumento 1 | configuración |
… | … |
Con base en el ejemplo anterior, configuramos la propiedad project
en la sección central ejecutando gcloud config set project <PROJECT_ID>
En otras palabras, set
es un comando.
Volviendo al comando gcloud config
, como se indica en su documentación oficial, gcloud config
es un grupo de comandos que le permite modificar propiedades. El uso es como tal:
gcloud config GROUP | COMMAND [GCLOUD_WIDE_FLAG … ]
por lo que COMMAND puede ser set
, list
, etc. (Tenga en cuenta que GROUP es config
)
Volviendo al uso del grupo de comandos gcloud config
, las opciones, en este caso, son GCLOUD_WIDE_FLAG
.
Por ejemplo, digamos que queremos mostrar el uso detallado y la descripción del comando, ejecutamos gcloud config set –help
. En otras palabras, --help
es la opción.
Otro ejemplo es cuando queremos establecer la propiedad zone en la sección de cómputo de un proyecto específico, ejecutamos gcloud config set compute <ZONE_NAME> –project=<PROJECT_ID>
. En otras palabras, --project
es una opción que contiene el valor <PROJECT_ID>
.
Por ejemplo, cuando queremos crear un clúster de dataproc, ejecutamos gcloud dataproc clusters create <CLUSTER_NAME> –region=<REGION>
. Y como se indica en su documentación de uso:
gcloud dataproc clusters create (CLUSTER: –region=REGION)
El indicador --region
es obligatorio si no se ha configurado previamente.
Las opciones cortas comienzan con -
seguidas de un solo carácter alfanumérico, mientras que las opciones largas comienzan con --
seguidas de varios caracteres. Piense en las opciones cortas como atajos cuando el usuario está seguro de lo que quiere, mientras que las opciones largas son más legibles.
¡Elegiste roca! La computadora ahora hará su selección.
Su equipo usa Trello para realizar un seguimiento de los problemas y el progreso del proyecto. Su equipo está buscando una forma más simplificada de interactuar con el tablero, algo similar a crear un nuevo repositorio de GitHub a través de la terminal. El equipo recurrió a usted para crear un programa CLI con este requisito básico de poder agregar una nueva tarjeta a la columna 'To Do' del tablero.
Requerimientos funcionales
Requerimientos no funcionales
Requisitos opcionales
PD No te preocupes por las dos últimas columnas, lo aprenderemos más tarde...
Pruebas unitarias
Trello
CLI
Utilidades (Varios)
Parte 1
py-trello
Parte 2
parte 3
¡La computadora eligió tijeras! A ver quien gana esta batalla...
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
: actúa como el nombre del paquete que usarán los usuarios, por ejemplo, pip install trellocli
__init__.py
: representa la raíz del paquete, conforma la carpeta como un paquete de Python__main__.py
: define el punto de entrada y permite a los usuarios ejecutar módulos sin especificar la ruta del archivo usando el indicador -m
, por ejemplo, python -m <module_name>
para reemplazar python -m <parent_folder>/<module_name>.py
models.py
: almacena clases utilizadas globalmente, por ejemplo, modelos a los que se espera que se ajusten las respuestas de la APIcli.py
: almacena la lógica comercial para los comandos y opciones de la CLItrelloservice.py
: almacena la lógica comercial para interactuar con py-trello
tests
: almacena pruebas unitarias para el programatest_cli.py
: almacena pruebas unitarias para la implementación de CLItest_trelloservice.py
: almacena pruebas unitarias para la interacción con py-trello
README.md
: almacena documentación para el programapyproject.toml
: almacena las configuraciones y requisitos del paquete.env
: almacena variables de entorno.gitignore
: especifica los archivos que se ignorarán (no se rastrearán) durante el control de versiones
Comenzando con el archivo __init__.py
en nuestro paquete, que sería donde se almacenan las constantes y variables del paquete, como el nombre y la versión de la aplicación. En nuestro caso, queremos inicializar lo siguiente:
# 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" }
Pasando al archivo __main__.py
, el flujo principal de su programa debe almacenarse aquí. En nuestro caso, almacenaremos el punto de entrada del programa CLI, asumiendo que habrá una función invocable en 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()
Ahora que se ha configurado el paquete, echemos un vistazo a la actualización de nuestro archivo README.md
(documentación principal). No hay una estructura específica que debamos seguir, pero un buen LÉAME consistiría en lo siguiente:
<!--- 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" = ""
El siguiente en la lista sería nuestro archivo .env
donde almacenamos nuestras variables de entorno, como claves y secretos de API. Es importante tener en cuenta que Git no debe realizar un seguimiento de este archivo, ya que contiene información confidencial.
En nuestro caso, almacenaremos aquí nuestras credenciales de Trello. Para crear un Power-Up en Trello, siga . Más específicamente, en función del uso de py-trello
, ya que tenemos la intención de usar OAuth para nuestra aplicación, necesitaremos lo siguiente para interactuar con Trello:
Una vez que haya recuperado su clave API y su secreto, guárdelos en el archivo .env
como tal
# .env TRELLO_API_KEY=<your_api_key> TRELLO_API_SECRET=<your_api_secret>
Por último, pero no menos importante, usemos la plantilla Python .gitignore
que se puede encontrar . Tenga en cuenta que esto es crucial para garantizar que nunca se rastree nuestro archivo .env
: si en algún momento se rastreó nuestro archivo .env
, incluso si eliminamos el archivo en pasos posteriores, el daño ya está hecho y los actores malintencionados pueden rastrear el archivo anterior. parches para información sensible.
Ahora que la configuración está completa, subamos nuestros cambios a GitHub. Según los metadatos especificados en pyproject.toml
, recuerde actualizar su LICENCIA y la URL de la página de inicio en consecuencia. Para referencia sobre cómo escribir mejores confirmaciones:
Según los requisitos funcionales, nuestra principal preocupación es permitir que los usuarios agreguen una nueva tarjeta. Haciendo referencia al método en py-trello
: . Para poder hacerlo, debemos llamar al método add_card
de la clase List
, del cual se puede recuperar de la función get_list
de la clase Board
, del cual se puede recuperar…
Antes de profundizar en el código, modifiquemos nuestro archivo pyproject.toml
para incluir las dependencias necesarias para escribir/ejecutar pruebas unitarias.
# pyproject.toml [project] dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1" ]
A continuación, activemos nuestro virtualenv y ejecutemos pip install .
para instalar las dependencias.
# 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 en mi código de muestra que GetOAuthTokenResponse
es un modelo que aún no se ha configurado en models.py
. Proporciona estructura para escribir código más limpio, lo veremos en acción más adelante.
Para ejecutar nuestras pruebas, simplemente ejecute python -m pytest
. Observe cómo fallarán nuestras pruebas, pero está bien, al final funcionará.
Rincón del desafío 💡 ¿Puedes intentar escribir más pruebas por tu cuenta? Siéntase libre de consultar para ver cómo se ven mis pruebas
Por ahora, construyamos nuestro trelloservice
. Comenzando con la adición de una nueva dependencia, ese es el contenedor py-trello
.
# pyproject.toml dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1", "py-trello==0.19.0" ]
Una vez más, ejecute pip install .
para instalar las dependencias.
Ahora, comencemos por construir nuestros modelos, para regular las respuestas que esperamos en trelloservice
. Para esta parte, es mejor consultar nuestras pruebas unitarias y el código fuente de py-trello
para comprender el tipo de valor de retorno que podemos esperar.
Por ejemplo, digamos que queremos recuperar el token de acceso del usuario, haciendo referencia a la función create_oauth_token
de py-trello
( ), sabemos que esperamos que el valor de retorno sea algo como esto
# 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 otro lado, tenga cuidado con las convenciones de nomenclatura conflictivas. Por ejemplo, el módulo py-trello
tiene una clase llamada List
. Una solución para esto sería proporcionar un alias durante la importación.
# 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
Rincón del desafío 💡 ¿Puedes intentar escribir más modelos por tu cuenta? Siéntase libre de consultar para ver cómo se ven mis modelos
Modelos abajo, comencemos oficialmente a codificar el trelloservice
. Nuevamente, debemos referirnos a las pruebas unitarias que creamos; digamos que la lista actual de pruebas no brinda una cobertura completa para el servicio, siempre regrese y agregue más pruebas cuando sea necesario.
Como de costumbre, incluya todas las declaraciones de importación hacia la parte superior. Luego cree la clase TrelloService
y los métodos de marcador de posición como se esperaba. La idea es que inicialicemos una instancia compartida del servicio en cli.py
y llamemos a sus métodos en consecuencia. Además, nuestro objetivo es la escalabilidad, por lo tanto, la necesidad de una amplia 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
Ps observe cómo esta vez cuando ejecutamos nuestras pruebas, nuestras pruebas pasarán. De hecho, esto nos ayudará a asegurarnos de mantenernos en el camino correcto. El flujo de trabajo debe ser para ampliar nuestras funciones, ejecutar nuestras pruebas, verificar si pasa/falla y refactorizar en consecuencia.
Comencemos con la función __init__
. La idea es llamar a la función get_user_oauth_token
aquí e inicializar TrelloClient
. Nuevamente, enfatizando la necesidad de almacenar dicha información confidencial solo en el archivo .env
, usaremos la dependencia python-dotenv
para recuperar información confidencial. Después de modificar nuestro archivo pyproject.toml
en consecuencia, comencemos a implementar los pasos de autorización.
# 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 )
En esta implementación, creamos un método auxiliar para manejar cualquier error previsible, por ejemplo, cuando el usuario hace clic en Deny
durante la autorización. Además, está configurado para solicitar de forma recursiva la autorización del usuario hasta que se devuelva una respuesta válida, porque el hecho es que no podemos continuar a menos que el usuario autorice nuestra aplicación para acceder a los datos de su cuenta.
Rincón del desafío 💡 ¿Aviso TRELLO_AUTHORIZATION_ERROR
? ¿Puedes declarar este error como una constante del paquete? Consulte Configuración para obtener más información.
# 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 )
En cuanto a la recuperación de las listas (columnas), tendremos que revisar la clase Board
de py-trello
, o en otras palabras, debemos aceptar un nuevo parámetro del tipo Board
value.
# 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 )
Rincón del desafío 💡 ¿Podrías implementar la función get_all_labels
y get_label
por tu cuenta? Revisar la clase Board
de py-trello
. Siéntase libre de consultar para ver cómo se ve mi implementación
# 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 )
¡Felicidades! Ganaste. ¿Reproducir de nuevo (sí/no)?