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 ¿Qué es un programa CLI? 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 , por ejemplo, una herramienta incluida con cualquier sistema UNIX que se puede activar simplemente ejecutando en la terminal. vim vim <FILE> Con respecto a la , profundicemos en la anatomía de un programa CLI. CLI de Google Cloud Argumentos 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 en la sección central, ejecutamos project gcloud config set project <PROJECT_ID> En particular, podemos traducir esto en Argumento Contenido argumento 0 gcloud argumento 1 configuración … … Comandos Los comandos son una serie de argumentos que proporcionan instrucciones a la computadora. Con base en el ejemplo anterior, configuramos la propiedad en la sección central ejecutando project gcloud config set project <PROJECT_ID> En otras palabras, es un comando. set Comandos opcionales 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 , como se indica en su documentación oficial, es un grupo de comandos que le permite modificar propiedades. El uso es como tal: gcloud config gcloud config gcloud config GROUP | COMMAND [GCLOUD_WIDE_FLAG … ] por lo que COMMAND puede ser , , etc. (Tenga en cuenta que GROUP es ) set list config Opciones 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 , las opciones, en este caso, son . gcloud config GCLOUD_WIDE_FLAG Por ejemplo, digamos que queremos mostrar el uso detallado y la descripción del comando, ejecutamos . En otras palabras, es la opción. gcloud config set –help --help Otro ejemplo es cuando queremos establecer la propiedad zone en la sección de cómputo de un proyecto específico, ejecutamos . En otras palabras, es una opción que contiene el valor . gcloud config set compute <ZONE_NAME> –project=<PROJECT_ID> --project <PROJECT_ID> También es importante tener en cuenta que sus posiciones generalmente no importan. Opciones obligatorias 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 . Y como se indica en su documentación de uso: gcloud dataproc clusters create <CLUSTER_NAME> –region=<REGION> gcloud dataproc clusters create (CLUSTER: –region=REGION) El indicador es obligatorio si no se ha configurado previamente. --region Opciones cortas frente a opciones largas 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. ¿Qué lograremos con este tutorial? 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: Esquema y objetivos 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 El usuario puede agregar una nueva tarjeta a una columna en el tablero Entradas requeridas: columna, nombre de la tarjeta Entradas opcionales: descripción de la tarjeta, etiquetas de la tarjeta (seleccione entre las existentes) Requerimientos no funcionales Programa para solicitar al usuario que proporcione acceso a la cuenta de Trello (autorización) Programa para solicitar al usuario que establezca en qué placa de Trello trabajar (configuración) Requisitos opcionales El usuario puede agregar una nueva columna al tablero El usuario puede agregar una nueva etiqueta al tablero El usuario puede ver una vista simplificada/detallada de todas las columnas 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 pytest pytest-simulacro cli-test-ayudantes Trello py-trello (envoltura de Python para el SDK de Trello) CLI tipeador rico menú-término-simple Utilidades (Varios) python-dotenv Cronología Abordaremos este proyecto en partes y aquí hay un fragmento de lo que puede esperar: Parte 1 Implementación de la lógica de negocios py-trello Parte 2 Implementación de lógica de negocios CLI Distribuir el programa CLI como un paquete parte 3 Implementación de requisitos funcionales opcionales Actualización del paquete ¡La computadora eligió tijeras! A ver quien gana esta batalla... Empecemos Estructura de carpetas El objetivo es distribuir el programa CLI como un paquete en . Por lo tanto, tal configuración es necesaria: PyPI 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: : actúa como el nombre del paquete que usarán los usuarios, por ejemplo, trellocli pip install trellocli : representa la raíz del paquete, conforma la carpeta como un paquete de Python __init__.py : define el punto de entrada y permite a los usuarios ejecutar módulos sin especificar la ruta del archivo usando el indicador , por ejemplo, para reemplazar __main__.py -m python -m <module_name> python -m <parent_folder>/<module_name>.py : almacena clases utilizadas globalmente, por ejemplo, modelos a los que se espera que se ajusten las respuestas de la API models.py : almacena la lógica comercial para los comandos y opciones de la CLI cli.py : almacena la lógica comercial para interactuar con trelloservice.py py-trello : almacena pruebas unitarias para el programa tests : almacena pruebas unitarias para la implementación de CLI test_cli.py : almacena pruebas unitarias para la interacción con test_trelloservice.py py-trello : almacena documentación para el programa README.md : almacena las configuraciones y requisitos del paquete pyproject.toml : almacena variables de entorno .env : especifica los archivos que se ignorarán (no se rastrearán) durante el control de versiones .gitignore 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 Configuración Antes de comenzar, veamos cómo configurar el paquete. Comenzando con el archivo 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: __init__.py nombre de la aplicación versión Constantes de ÉXITO y ERROR # 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 , 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 __main__.py 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 (documentación principal). No hay una estructura específica que debamos seguir, pero un buen LÉAME consistiría en lo siguiente: README.md Descripción general Instalación y Requisitos Primeros pasos y uso 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 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. .env 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 , ya que tenemos la intención de usar OAuth para nuestra aplicación, necesitaremos lo siguiente para interactuar con Trello: esta guía py-trello Clave API (para nuestra aplicación) API Secret (para nuestra aplicación) Token (token del usuario para otorgar acceso a sus datos) Una vez que haya recuperado su clave API y su secreto, guárdelos en el archivo como tal .env # .env TRELLO_API_KEY=<your_api_key> TRELLO_API_SECRET=<your_api_secret> Por último, pero no menos importante, usemos la plantilla Python que se puede encontrar . Tenga en cuenta que esto es crucial para garantizar que nunca se rastree nuestro archivo : si en algún momento se rastreó nuestro archivo , 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. .gitignore aquí .env .env Ahora que la configuración está completa, subamos nuestros cambios a GitHub. Según los metadatos especificados en , recuerde actualizar su LICENCIA y la URL de la página de inicio en consecuencia. Para referencia sobre cómo escribir mejores confirmaciones: pyproject.toml Write Better Commits, Build Better Projects por Victoria Dye Otros pasos destacados: Construir un virtualenv para el proyecto. Pruebas unitarias 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 : . Para poder hacerlo, debemos llamar al método de la clase , del cual se puede recuperar de la función de la clase , del cual se puede recuperar… py-trello add_card add_card List get_list Board Entiendes la esencia: necesitaremos muchos métodos de ayuda para llegar a nuestro destino final, digámoslo en palabras: Prueba para recuperar el token del cliente Prueba para recuperar tableros Prueba para recuperar un tablero Prueba para recuperar listas del tablero Prueba para recuperar una lista Prueba para recuperar etiquetas del tablero Prueba para recuperar una etiqueta Prueba para agregar tarjeta Prueba para agregar etiqueta a la tarjeta 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 para incluir las dependencias necesarias para escribir/ejecutar pruebas unitarias. pyproject.toml # pyproject.toml [project] dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1" ] A continuación, activemos nuestro virtualenv y ejecutemos para instalar las dependencias. pip install . 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 es un modelo que aún no se ha configurado en . Proporciona estructura para escribir código más limpio, lo veremos en acción más adelante. GetOAuthTokenResponse models.py Para ejecutar nuestras pruebas, simplemente ejecute . Observe cómo fallarán nuestras pruebas, pero está bien, al final funcionará. python -m pytest 💡 ¿Puedes intentar escribir más pruebas por tu cuenta? Siéntase libre de consultar para ver cómo se ven mis pruebas Rincón del desafío este parche Por ahora, construyamos nuestro . Comenzando con la adición de una nueva dependencia, ese es el contenedor . trelloservice py-trello # pyproject.toml dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1", "py-trello==0.19.0" ] Una vez más, ejecute para instalar las dependencias. pip install . Modelos Ahora, comencemos por construir nuestros modelos, para regular las respuestas que esperamos en . Para esta parte, es mejor consultar nuestras pruebas unitarias y el código fuente de para comprender el tipo de valor de retorno que podemos esperar. trelloservice py-trello Por ejemplo, digamos que queremos recuperar el token de acceso del usuario, haciendo referencia a la función de ( ), sabemos que esperamos que el valor de retorno sea algo como esto create_oauth_token py-trello código fuente # 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 tiene una clase llamada . Una solución para esto sería proporcionar un alias durante la importación. py-trello List # 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 💡 ¿Puedes intentar escribir más modelos por tu cuenta? Siéntase libre de consultar para ver cómo se ven mis modelos Rincón del desafío este parche Lógica de negocios Configuración Modelos abajo, comencemos oficialmente a codificar el . 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. trelloservice Como de costumbre, incluya todas las declaraciones de importación hacia la parte superior. Luego cree la clase y los métodos de marcador de posición como se esperaba. La idea es que inicialicemos una instancia compartida del servicio en y llamemos a sus métodos en consecuencia. Además, nuestro objetivo es la escalabilidad, por lo tanto, la necesidad de una amplia cobertura. TrelloService cli.py # 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. Autorización e inicialización de TrelloClient Comencemos con la función . La idea es llamar a la función aquí e inicializar . Nuevamente, enfatizando la necesidad de almacenar dicha información confidencial solo en el archivo , usaremos la dependencia para recuperar información confidencial. Después de modificar nuestro archivo en consecuencia, comencemos a implementar los pasos de autorización. __init__ get_user_oauth_token TrelloClient .env python-dotenv pyproject.toml # 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 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. Deny 💡 ¿Aviso ? ¿Puedes declarar este error como una constante del paquete? Consulte Configuración para obtener más información. Rincón del desafío TRELLO_AUTHORIZATION_ERROR Funciones auxiliares 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 de , o en otras palabras, debemos aceptar un nuevo parámetro del tipo value. Board py-trello 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 ) 💡 ¿Podrías implementar la función y por tu cuenta? Revisar la clase de . Siéntase libre de consultar para ver cómo se ve mi implementación Rincón del desafío get_all_labels get_label Board py-trello este parche Función para agregar una nueva tarjeta 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)? Envolver 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