Descargo de responsabilidad: este tutorial asume que los lectores tienen un conocimiento básico de Python, API, Git y Unit Tests.
Me encontré con varios software CLI con las animaciones más geniales, y me hizo preguntarme: ¿podría alguna vez actualizar mi proyecto escolar 'minimalista' de piedra, papel o tijera?
¡Hola, vamos a jugar! Elige tu luchador (piedra, papel, tijera): roca
Como se indica en Wikipedia, "una interfaz de línea de comandos (CLI) es un medio para interactuar con un dispositivo o programa de computadora con comandos de un usuario o cliente, y respuestas del dispositivo o programa, en forma de líneas de texto".
En otras palabras, un programa CLI es un programa en el que el usuario utiliza la línea de comandos para interactuar con el programa proporcionando instrucciones para ejecutar.
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.
Con respecto a la CLI de Google Cloud , profundicemos en la anatomía de un programa CLI.
Los argumentos (parámetros) son elementos de información proporcionados a un programa. A menudo se los denomina argumentos posicionales porque se identifican por su posición.
Por ejemplo, cuando queremos establecer la propiedad project
en la sección central, ejecutamos gcloud config set project <PROJECT_ID>
En particular, podemos traducir esto en
Argumento | Contenido |
---|---|
argumento 0 | gcloud |
argumento 1 | configuración |
… | … |
Los comandos son una serie de argumentos que proporcionan instrucciones a la computadora.
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.
Por lo general, se requieren comandos, pero podemos hacer excepciones. Según el caso de uso del programa, podemos definir comandos opcionales.
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
)
Las opciones son tipos documentados de parámetros que modifican el comportamiento de un comando. Son pares clave-valor que se indican con '-' o '--'.
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>
.
También es importante tener en cuenta que sus posiciones generalmente no importan.
Las opciones, como su nombre, suelen ser opcionales, pero también se pueden adaptar para que sean obligatorias.
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.
Así que mentí... No intentaremos actualizar el programa CLI básico de piedra, papel o tijera.
En su lugar, echemos un vistazo a un escenario del mundo real:
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.
Con base en el requisito mencionado, redactemos nuestro programa CLI definiendo sus requisitos:
Requerimientos funcionales
Requerimientos no funcionales
Requisitos opcionales
En base a lo anterior, podemos formalizar los comandos y opciones de nuestro programa CLI como tal:
PD No te preocupes por las dos últimas columnas, lo aprenderemos más tarde...
En cuanto a nuestra pila tecnológica, nos apegaremos a esto:
Pruebas unitarias
Trello
CLI
Utilidades (Varios)
Abordaremos este proyecto en partes y aquí hay un fragmento de lo que puede esperar:
Parte 1
py-trello
Parte 2
parte 3
¡La computadora eligió tijeras! A ver quien gana esta batalla...
El objetivo es distribuir el programa CLI como un paquete en PyPI . Por lo tanto, tal configuración es necesaria:
trellocli/ __init__.py __main__.py models.py cli.py trelloservice.py tests/ test_cli.py test_trelloservice.py README.md pyproject.toml .env .gitignore
Aquí hay una inmersión profunda en cada archivo y/o directorio:
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
Para obtener una explicación más detallada de la publicación de paquetes de Python, aquí hay un excelente artículo para consultar: Cómo publicar un paquete de Python de código abierto en PyPI por Geir Arne Hjelle
Antes de comenzar, veamos cómo configurar el paquete.
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:
Otra gran publicación para leer si desea profundizar más: Cómo escribir un buen LÉAME por merlos
Así es como me gustaría estructurar el LÉAME para este proyecto
<!--- README.md --> # Overview # Getting Started # Usage # Architecture ## Data Flow ## Tech Stack # Running Tests # Next Steps # References
Dejemos el esqueleto como está por ahora; volveremos a esto más tarde.
Continuando, configuremos los metadatos de nuestro paquete según la documentación 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 cómo hay marcadores de posición que debe modificar, por ejemplo, su nombre de usuario, su nombre...
En otra nota, dejaremos la URL de la página de inicio vacía por ahora. Haremos los cambios después de haberlo publicado en GitHub. También dejaremos la porción de dependencias vacía por ahora y agregaremos a medida que avanzamos.
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 esta guía . 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 aquí . 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: Write Better Commits, Build Better Projects por Victoria Dye
Otros pasos destacados:
Antes de comenzar a escribir nuestras pruebas, es importante tener en cuenta que debido a que estamos trabajando con una API, implementaremos pruebas simuladas para poder probar nuestro programa sin el riesgo de tiempo de inactividad de la API. Aquí hay otro excelente artículo sobre pruebas simuladas de Real Python: Simulación de API externas en Python
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
: add_card . 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…
Entiendes la esencia: necesitaremos muchos métodos de ayuda para llegar a nuestro destino final, digámoslo en palabras:
También es importante tener en cuenta que al escribir pruebas unitarias, queremos que nuestras pruebas sean lo más extensas posible. ¿Maneja bien los errores? ¿Cubre todos los aspectos de nuestro programa?
Sin embargo, solo para el propósito de este tutorial, simplificaremos las cosas al verificar solo los casos de éxito.
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.
Una vez hecho esto, escribamos finalmente algunas pruebas. En general, nuestras pruebas deben incluir una respuesta simulada para ser devuelta, un parche para la función que intentamos probar arreglando el valor de retorno con la respuesta simulada y, finalmente, una llamada a la función. Una prueba de muestra para recuperar los tokens de acceso del usuario sería la siguiente:
# 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 consultareste parche 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
( código fuente ), 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
No dude en aprovechar también esta oportunidad para adaptar los modelos a las necesidades de su programa. Por ejemplo, digamos que solo necesita un atributo del valor devuelto, podría refactorizar su modelo para esperar extraer dicho valor del valor devuelto en lugar de almacenarlo como un todo.
# 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 consultareste parche 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.
Ahora que la parte de la autorización está lista, pasemos a las funciones auxiliares, comenzando con la recuperación de los tableros de Trello del usuario.
# 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 consultareste parche para ver cómo se ve mi implementación
Por último, pero no menos importante, finalmente logramos lo que habíamos estado buscando todo este tiempo: agregar una nueva tarjeta. Tenga en cuenta que no usaremos todas las funciones declaradas anteriormente aquí: el objetivo de las funciones auxiliares es aumentar la escalabilidad.
# 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 )
🎉 Ahora que está hecho y desempolvado, recuerde actualizar su README en consecuencia y envíe su código a GitHub.
¡Felicidades! Ganaste. ¿Reproducir de nuevo (sí/no)?
Gracias por su paciencia :) A través de este tutorial, aprendió con éxito a implementar la simulación al escribir pruebas unitarias, estructurar modelos para la cohesión, leer el código fuente para encontrar funcionalidades clave e implementar la lógica comercial utilizando un contenedor de terceros.
Esté atento a la Parte 2, donde profundizaremos en la implementación del programa CLI en sí.
Mientras tanto, sigamos en contacto 👀
Código fuente de GitHub: https://github.com/elainechan01/trellocli