paint-brush
Cómo crear un programa CLI de Python para la gestión de tableros de Trello (Parte 2)por@elainechan01
1,777 lecturas
1,777 lecturas

Cómo crear un programa CLI de Python para la gestión de tableros de Trello (Parte 2)

por Elaine Yun Ru Chan27m2023/11/07
Read on Terminal Reader

Demasiado Largo; Para Leer

La parte 2 de la serie de tutoriales sobre Cómo crear un programa CLI de Python para Trello Board Management se centra en cómo escribir lógica empresarial para comandos CLI y distribución de paquetes Python.
featured image - Cómo crear un programa CLI de Python para la gestión de tableros de Trello (Parte 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

Ya hemos superado con creces el proyecto escolar básico de piedra, papel y tijera: profundicemos en él.


¿Qué conseguiremos con este tutorial?

En Cómo crear un programa CLI de Python para la administración de placas de Trello (Parte 1) , creamos con éxito la lógica empresarial para interactuar con el SDK de Trello.


Aquí hay un resumen rápido de la arquitectura de nuestro programa CLI:

Vista de tabla detallada de la estructura CLI según los requisitos


En este tutorial, veremos cómo transformar nuestro proyecto en un programa CLI, centrándonos en los requisitos funcionales y no funcionales.


Por otro lado, también aprenderemos cómo distribuir nuestro programa como un paquete en PyPI .


Empecemos


Estructura de carpetas

Anteriormente, logramos configurar un esqueleto para alojar nuestro módulo trelloservice . Esta vez queremos implementar una carpeta cli con módulos para diferentes funcionalidades, a saber:


  • configuración
  • acceso
  • lista


La idea es que, para cada grupo de comandos, sus comandos se almacenen en su propio módulo. En cuanto al comando list , lo almacenaremos en el archivo CLI principal ya que no pertenece a ningún grupo de comandos.


Por otro lado, veamos cómo limpiar nuestra estructura de carpetas. Más específicamente, deberíamos empezar a tener en cuenta la escalabilidad del software asegurándonos de que los directorios no estén saturados.


Aquí hay una sugerencia sobre nuestra estructura de carpetas:

 trellocli/ __init__.py __main__.py trelloservice.py shared/ models.py custom_exceptions.py cli/ cli.py cli_config.py cli_create.py tests/ test_cli.py test_trelloservice.py assets/ images/ README.md pyproject.toml .env .gitignore


¿Observas que está la carpeta assets ? Esto se usará para almacenar activos relacionados para nuestro README , mientras que al haber una carpeta shared recientemente implementada en trellocli , la usaremos para almacenar módulos que se usarán en todo el software.


Configuración

Comencemos modificando nuestro archivo de punto de entrada, __main__.py . En cuanto a la importación en sí, debido a que hemos decidido almacenar los módulos relacionados en sus propias subcarpetas, tendremos que adaptarnos a dichos cambios. Por otro lado, también asumimos que el módulo CLI principal, cli.py , tiene una instancia app que podemos ejecutar.

 # trellocli/__main__.py # module imports from trellocli import __app_name__ from trellocli.cli import cli from trellocli.trelloservice import TrelloService # dependencies imports # misc imports def main(): cli.app(prog_name=__app_name__) if __name__ == "__main__": main()


Avance rápido hasta nuestro archivo cli.py ; Almacenaremos nuestra instancia app aquí. La idea es inicializar un objeto Typer para compartirlo con todo el software.

 # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer()


Avanzando con este concepto, modifiquemos nuestro pyproject.toml para especificar nuestros scripts de línea de comando. Aquí, proporcionaremos un nombre para nuestro paquete y definiremos el punto de entrada.

 # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main"


Según el ejemplo anterior, definimos trellocli como el nombre del paquete y la función main en el script __main__ , que está almacenado en el módulo trellocli , se ejecutará durante el tiempo de ejecución.


Ahora que la parte CLI de nuestro software está configurada, modifiquemos nuestro módulo trelloservice para servir mejor a nuestro programa CLI. Como recordará, nuestro módulo trelloservice está configurado para solicitar de forma recursiva la autorización del usuario hasta que se apruebe. Modificaremos esto de manera que el programa se cierre si no se otorga autorización e instamos al usuario a ejecutar el comando config access . Esto asegurará que nuestro programa sea más limpio y más descriptivo en términos de instrucciones.


Para poner esto en palabras, modificaremos estas funciones:


  • __init__
  • __load_oauth_token_env_var
  • authorize
  • is_authorized


Comenzando con la función __init__ , inicializaremos un cliente vacío en lugar de manejar la configuración del cliente aquí.

 # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None


Rincón del desafío 💡 ¿Puedes modificar nuestra función __load_oauth_token_env_var para que no solicite de forma recursiva la autorización del usuario? Sugerencia: una función recursiva es una función que se llama a sí misma.


Pasando a las funciones auxiliares authorize e is_authorized , la idea es que authorize llevará a cabo la lógica empresarial de configurar el cliente utilizando la función __load_oauth_token_env_var mientras que la función is_authorized simplemente devuelve un valor booleano de si se concedió la autorización.

 # trellocli/trelloservice.py class TrelloService: def authorize(self) -> AuthorizeResponse: """Method to authorize program to user's trello account Returns AuthorizeResponse: success / error """ self.__load_oauth_token_env_var() load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): return AuthorizeResponse(status_code=TRELLO_AUTHORIZATION_ERROR) else: self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) return AuthorizeResponse(status_code=SUCCESS) def is_authorized(self) -> bool: """Method to check authorization to user's trello account Returns bool: authorization to user's account """ if not self.__client: return False else: return True


Comprenda que la diferencia entre __load_oauth_token_env_var y authorize es que __load_oauth_token_env_var es una función interna que sirve para almacenar el token de autorización como una variable de entorno, mientras que authorize es la función pública, intenta recuperar todas las credenciales necesarias e inicializar un Cliente Trello.


Rincón del desafío 💡Observe cómo nuestra función authorize devuelve un tipo de datos AuthorizeResponse . ¿Puedes implementar un modelo que tenga el atributo status_code ? Consulte la Parte 1 de Cómo crear un programa CLI de Python para la administración de tableros de Trello (Sugerencia: observe cómo creamos modelos)


Por último, creemos una instancia de un objeto TrelloService singleton hacia la parte inferior del módulo. No dudes en consultar este parche para ver cómo se ve el código completo: trello-cli-kit

 # trellocli/trelloservice.py trellojob = TrelloService()


Finalmente, queremos inicializar algunas excepciones personalizadas para compartirlas en todo el programa. Esto es diferente de los ERRORS definidos en nuestro inicializador, ya que estas excepciones son subclases de BaseException y actúan como excepciones típicas definidas por el usuario, mientras que los ERRORS sirven más como valores constantes que comienzan desde 0.


Mantengamos nuestras excepciones al mínimo y vayamos con algunos de los casos de uso comunes, en particular:


  • Error de lectura: aparece cuando hay un error de lectura de Trello
  • Error de escritura: aparece cuando hay un error al escribir en Trello
  • Error de autorización: surge cuando no se otorga autorización para Trello
  • Error de entrada de usuario no válida: aparece cuando no se reconoce la entrada CLI del usuario


 # trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass


Pruebas unitarias

Como se mencionó en la Parte I, no cubriremos extensamente las pruebas unitarias en este tutorial, así que trabajemos solo con los elementos necesarios:


  • Prueba para configurar el acceso
  • Prueba para configurar la placa trello
  • Prueba para crear una nueva tarjeta Trello
  • Prueba para mostrar los detalles del tablero Trello
  • Prueba para mostrar los detalles del tablero Trello (vista detallada)


La idea es burlarse de un intérprete de línea de comando, como un shell para probar los resultados esperados. Lo bueno del módulo Typer es que viene con su propio objeto runner . En cuanto a ejecutar las pruebas, las vincularemos con el módulo pytest . Para obtener más información, consulte los documentos oficiales de Typer .


Trabajemos juntos en la primera prueba, es decir, para configurar el acceso. Comprenda que estamos probando si la función se ejecuta correctamente. Para hacerlo, verificaremos la respuesta del sistema y si el código de salida es success , también conocido como 0. Aquí hay un excelente artículo de RedHat sobre qué son los códigos de salida y cómo los usa el sistema para comunicar procesos .

 # trellocli/tests/test_cli.py # module imports from trellocli.cli.cli import app # dependencies imports from typer.testing import CliRunner # misc imports runner = CliRunner() def test_config_access(): res = runner.invoke(app, ["config", "access"]) assert result.exit_code == 0 assert "Go to the following link in your browser:" in result.stdout


Rincón del desafío 💡Ahora que entiendes lo esencial, ¿puedes implementar otros casos de prueba por tu cuenta? (Pista: también deberías considerar realizar pruebas para detectar casos de falla)


Lógica de negocios


Módulo CLI principal

Comprenda que este será nuestro módulo cli principal: para todos los grupos de comandos (config, create), su lógica de negocios se almacenará en su propio archivo separado para una mejor legibilidad.


En este módulo, almacenaremos nuestro comando list . Profundizando en el comando, sabemos que queremos implementar las siguientes opciones:


  • board_name: requerido si config board no se configuró previamente
  • detallado: mostrar en una vista detallada


Comenzando con la opción requerida board_name, hay algunas formas de lograr esto, una de ellas es usar la función de devolución de llamada (para obtener más información, aquí están los documentos oficiales ) o simplemente usar una variable de entorno predeterminada. Sin embargo, para nuestro caso de uso, hagámoslo sencillo generando nuestra excepción personalizada InvalidUserInputError si no se cumplen las condiciones.


Para desarrollar el comando, comencemos definiendo las opciones. En Typer, como se menciona en sus documentos oficiales , los ingredientes clave para definir una opción serían:


  • Tipo de datos
  • Texto de ayuda
  • Valor por defecto


Por ejemplo, para crear la opción detailed con las siguientes condiciones:


  • Tipo de datos: booleano
  • Texto de ayuda: "Habilitar vista detallada"
  • Valor predeterminado: Ninguno


Nuestro código se vería así:

 detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None


En general, para definir el comando list con las opciones necesarias, trataremos list como una función de Python y sus opciones como parámetros obligatorios.

 # trellocli/cli/cli.py @app.command() def list( detailed: Annotated[bool, Option(help="Enable detailed view")] = None, board_name: Annotated[str, Option(help="Trello board to search")] = "" ) -> None: pass


Tenga en cuenta que estamos agregando el comando a la instancia de la app inicializada en la parte superior del archivo. Siéntete libre de navegar por el código base oficial de Typer para modificar las Opciones a tu gusto.


En cuanto al flujo de trabajo del comando, optaremos por algo como esto:


  • Verificar autorización
  • Configure para usar el tablero apropiado (verifique si se proporcionó la opción board_name )
  • Configurar el tablero Trello para poder leerlo
  • Recupere los datos apropiados de la tarjeta Trello y clasifíquelos según la lista de Trello
  • Mostrar datos (comprobar si se seleccionó la opción detailed )


Algunas cosas a tener en cuenta...


  • Queremos generar excepciones cuando trellojob produzca un código de estado distinto de SUCCESS . Al utilizar bloques try-catch , podemos evitar que nuestro programa sufra fallos fatales.
  • Al configurar el tablero apropiado para usar, intentaremos configurar el tablero Trello para su uso según el board_id recuperado. Por lo tanto, queremos cubrir los siguientes casos de uso.
    • Recuperar board_id si board_name se proporcionó explícitamente verificando una coincidencia usando la función get_all_boards en trellojob
    • Recuperar el board_id almacenado como una variable de entorno si no se usó la opción board_name
  • Los datos que mostraremos se formatearán utilizando la funcionalidad Table del paquete rich . Para obtener más información sobre rich , consulte sus documentos oficiales.
    • Detallado: muestra un resumen de la cantidad de listas de Trello, la cantidad de tarjetas y etiquetas definidas. Para cada lista de Trello, muestre todas las tarjetas y sus correspondientes nombres, descripciones y etiquetas asociadas.
    • No detallado: muestra un resumen de la cantidad de listas de Trello, la cantidad de tarjetas y las etiquetas definidas.


Juntando todo, obtenemos lo siguiente. Descargo de responsabilidad: es posible que falten algunas funciones de TrelloService que aún tenemos que implementar. Consulte este parche si necesita ayuda para implementarlo: trello-cli-kit

 # trellocli/cli/cli.py # module imports from trellocli.trelloservice import trellojob from trellocli.cli import cli_config, cli_create from trellocli.misc.custom_exceptions import * from trellocli import SUCCESS # dependencies imports from typer import Typer, Option from rich import print from rich.console import Console from rich.table import Table from dotenv import load_dotenv # misc imports from typing_extensions import Annotated import os # singleton instances app = Typer() console = Console() # init command groups app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") app.add_typer(cli_create.app, name="create", help="COMMAND GROUP to create new Trello elements") @app.command() def list( detailed: Annotated[ bool, Option(help="Enable detailed view") ] = None, board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to list board details in a simplified (default)/detailed view OPTIONS detailed (bool): request for detailed view board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError labels_list = res_get_all_labels.res res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError trellolists_list = res_get_all_lists.res # store data on cards for each trellolist trellolists_dict = {trellolist: [] for trellolist in trellolists_list} for trellolist in trellolists_list: res_get_all_cards = trellojob.get_all_cards(trellolist=trellolist) if res_get_all_cards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving cards from trellolist") raise TrelloReadError cards_list = res_get_all_cards.res trellolists_dict[trellolist] = cards_list # display data (lists count, cards count, labels) # if is_detailed OPTION is selected, display data (name, description, labels) for each card in each trellolist print() table = Table(title="Board: "+board.name, title_justify="left", show_header=False) table.add_row("[bold]Lists count[/]", str(len(trellolists_list))) table.add_row("[bold]Cards count[/]", str(sum([len(cards_list) for cards_list in trellolists_dict.values()]))) table.add_row("[bold]Labels[/]", ", ".join([label.name for label in labels_list if label.name])) console.print(table) if detailed: for trellolist, cards_list in trellolists_dict.items(): table = Table("Name", "Desc", "Labels", title="List: "+trellolist.name, title_justify="left") for card in cards_list: table.add_row(card.name, card.description, ", ".join([label.name for label in card.labels if label.name])) console.print(table) print() except (AuthorizationError, InvalidUserInputError, TrelloReadError): print("Program exited...")


Para ver nuestro software en acción, simplemente ejecute python -m trellocli --help en la terminal. De forma predeterminada, el módulo Typer completará la salida del comando --help por sí solo. Y observe cómo podemos llamar trellocli como nombre del paquete. ¿Recuerda cómo se definió esto previamente en nuestro pyproject.toml ?


Avancemos un poco e inicialicemos también los grupos de comandos create y config . Para hacerlo, simplemente usaremos la función add_typer en nuestro objeto app . La idea es que el grupo de comandos tenga su propio objeto app , y simplemente lo agregaremos a la app principal en cli.py , junto con el nombre del grupo de comandos y el texto de ayuda. Debería verse algo como esto

 # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations")


Rincón del desafío 💡 ¿Podrías importar el grupo de comandos create por tu cuenta? No dude en consultar este parche para obtener ayuda: trello-cli-kit


Subcomandos

Para configurar un grupo de comandos para create , almacenaremos sus respectivos comandos en su propio módulo. La configuración es similar a la de cli.py con la necesidad de crear una instancia de un objeto Typer. En cuanto a los comandos, también nos gustaría cumplir con la necesidad de utilizar excepciones personalizadas. Un tema adicional que queremos cubrir es cuando el usuario presiona Ctrl + C , o en otras palabras, interrumpe el proceso. La razón por la que no cubrimos esto para nuestro comando list es porque la diferencia aquí es que el grupo de comandos config consta de comandos interactivos. La principal diferencia entre los comandos interactivos es que requieren una interacción continua del usuario. Eso sí, decir que nuestro comando directo tarda bastante en ejecutarse. También es una buena práctica manejar posibles interrupciones del teclado.


Comenzando con el comando access , finalmente usaremos la función authorize creada en nuestro TrelloService . Dado que la función authorize maneja la configuración por sí sola, solo tendremos que verificar la ejecución del proceso.

 # trellocli/cli/cli_config.py @app.command() def access() -> None: """COMMAND to configure authorization for program to access user's Trello account""" try: # check authorization res_authorize = trellojob.authorize() if res_authorize.status_code != SUCCESS: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except AuthorizationError: print("Program exited...")


En cuanto al comando de la board , utilizaremos varios módulos para brindar una buena experiencia de usuario, incluido el Menú de terminal simple para mostrar una GUI de terminal para la interacción del usuario. La idea principal es la siguiente:


  • Verificar autorización
  • Recuperar todos los tableros de Trello de la cuenta del usuario
  • Mostrar un menú de terminal de selección única de tableros Trello
  • Establecer la ID del tablero Trello seleccionado como una variable de entorno


 # trellocli/cli/cli_config.py @app.command() def board() -> None: """COMMAND to initialize Trello board""" try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # retrieve all boards res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving trello boards") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # for easy access to board id when given board name # display menu to select board boards_menu = TerminalMenu( boards_list.keys(), title="Select board:", raise_error_on_interrupt=True ) boards_menu.show() selected_board = boards_menu.chosen_menu_entry # set board ID as env var dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_BOARD_ID", value_to_set=boards_list[selected_board] ) except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, TrelloReadError): print("Program exited...")


Finalmente, pasamos al requisito funcional principal de nuestro software: agregar una nueva tarjeta a una lista en el tablero de Trello. Usaremos los mismos pasos desde nuestro comando list hasta recuperar datos del tablero.


Aparte de eso, solicitaremos interactivamente la entrada del usuario para configurar correctamente la nueva tarjeta:


  • Lista de Trello a agregar a: Selección única
  • Nombre de la tarjeta: Texto
  • [Opcional] Descripción de la tarjeta: Texto
  • [Opcional] Etiquetas: Selección múltiple
  • Confirmación: sí/no


Para todas las indicaciones que requieren que el usuario seleccione de una lista, usaremos el paquete Simple Terminal Menu como antes. En cuanto a otras indicaciones y elementos diversos, como la necesidad de ingresar texto o la confirmación del usuario, simplemente usaremos el paquete rich . También es importante tener en cuenta que debemos manejar adecuadamente los valores opcionales:


  • Los usuarios pueden omitir proporcionar una descripción.
  • Los usuarios pueden proporcionar una selección vacía para Etiquetas


 # trellocli/cli/cli_create.py @app.command() def card( board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to add a new trello card OPTIONS board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the labels from the trello board") raise TrelloReadError labels_list = res_get_all_labels.res labels_dict = {label.name: label for label in labels_list if label.name} res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the lists from the trello board") raise TrelloReadError trellolists_list = res_get_all_lists.res trellolists_dict = {trellolist.name: trellolist for trellolist in trellolists_list} # for easy access to trellolist when given name of trellolist # request for user input (trellolist, card name, description, labels to include) interactively to configure new card to be added trellolist_menu = TerminalMenu( trellolists_dict.keys(), title="Select list:", raise_error_on_interrupt=True ) # Prompt: trellolist trellolist_menu.show() print(trellolist_menu.chosen_menu_entry) selected_trellolist = trellolists_dict[trellolist_menu.chosen_menu_entry] selected_name = Prompt.ask("Card name") # Prompt: card name selected_desc = Prompt.ask("Description (Optional)", default=None) # Prompt (Optional) description labels_menu = TerminalMenu( labels_dict.keys(), title="Select labels (Optional):", multi_select=True, multi_select_empty_ok=True, multi_select_select_on_accept=False, show_multi_select_hint=True, raise_error_on_interrupt=True ) # Prompt (Optional): labels labels_menu.show() selected_labels = [labels_dict[label] for label in list(labels_menu.chosen_menu_entries)] if labels_menu.chosen_menu_entries else None # display user selection and request confirmation print() confirmation_table = Table(title="Card to be Added", show_header=False) confirmation_table.add_row("List", selected_trellolist.name) confirmation_table.add_row("Name", selected_name) confirmation_table.add_row("Description", selected_desc) confirmation_table.add_row("Labels", ", ".join([label.name for label in selected_labels]) if selected_labels else None) console.print(confirmation_table) confirm = Confirm.ask("Confirm") # if confirm, attempt to add card to trello # else, exit if confirm: res_add_card = trellojob.add_card( col=selected_trellolist, name=selected_name, desc=selected_desc, labels=selected_labels ) if res_add_card.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when adding a new card to trello") raise TrelloWriteError else: print("Process cancelled...") except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, InvalidUserInputError, TrelloReadError, TrelloWriteError): print("Program exited...")


Rincón del desafío 💡¿Puedes mostrar una barra de progreso para el proceso add ? Sugerencia: eche un vistazo al uso de la función de estado de rich


Distribución de paquetes

Aquí viene la parte divertida: distribuir oficialmente nuestro software en PyPI. Seguiremos este canal para hacerlo:


  • Configurar metadatos + actualizar README
  • Subir para probar PyPI
  • Configurar acciones de GitHub
  • Insertar código para etiquetar v1.0.0
  • Distribuir código a PyPI 🎉


Para obtener una explicación detallada, consulte este excelente tutorial sobre Python Packaging de Ramit Mittal.


Configuración de metadatos

El último detalle que necesitamos para nuestro pyproject.toml es especificar qué módulo almacena el paquete en sí. En nuestro caso, será trellocli . Aquí están los metadatos para agregar:

 # pyproject.toml [tool.setuptools] packages = ["trellocli"]


En cuanto a nuestro README.md , es una buena práctica proporcionar algún tipo de guía, ya sean pautas de uso o cómo comenzar. Si incluyó imágenes en su README.md , debe usar su URL absoluta, que generalmente tiene el siguiente formato

 https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image>


PruebaPyPI

Usaremos las herramientas build y twine para crear y publicar nuestro paquete. Ejecute el siguiente comando en su terminal para crear un archivo fuente y una rueda para su paquete:

 python -m build


Asegúrese de tener una cuenta configurada en TestPyPI y ejecute el siguiente comando

 twine upload -r testpypi dist/*


Se le pedirá que escriba su nombre de usuario y contraseña. Debido a que tiene habilitada la autenticación de dos factores, se le pedirá que utilice un token API (para obtener más información sobre cómo adquirir un token API TestPyPI: enlace a la documentación ). Simplemente ingrese los siguientes valores:


  • nombre de usuario: simbólico
  • contraseña: <su token TestPyPI>


Una vez que se haya completado, debería poder dirigirse a TestPyPI para ver su paquete recién distribuido.


Configuración de GitHub

El objetivo es utilizar GitHub como un medio para actualizar continuamente nuevas versiones de su paquete en función de las etiquetas.


Primero, dirígete a la pestaña Actions en tu flujo de trabajo de GitHub y selecciona un nuevo flujo de trabajo. Usaremos el flujo de trabajo Publish Python Package creado por GitHub Actions. ¿Observa cómo el flujo de trabajo requiere la lectura de los secretos del repositorio? Asegúrese de haber almacenado su token PyPI con el nombre especificado (adquirir un token API de PyPI es similar a TestPyPI).


Una vez creado el flujo de trabajo, enviaremos nuestro código a la etiqueta v1.0.0. Para obtener más información sobre la sintaxis de nomenclatura de versiones, aquí hay una excelente explicación de Py-Pkgs: enlace a la documentación


Simplemente ejecute los comandos habituales pull , add y commit . A continuación, cree una etiqueta para su última confirmación ejecutando el siguiente comando (para obtener más información sobre las etiquetas: enlace a la documentación )

 git tag <tagname> HEAD


Finalmente, envíe su nueva etiqueta al repositorio remoto.

 git push <remote name> <tag name>


Aquí hay un excelente artículo de Karol Horosin sobre la integración de CI/CD con su paquete Python si desea obtener más información. Pero por ahora, siéntate y disfruta de tu último logro 🎉. Siéntase libre de ver cómo se desarrolla la magia como un flujo de trabajo de GitHub Actions mientras distribuye su paquete a PyPI.


Envolver

Este fue largo 😓. A través de este tutorial, aprendió a transformar su software en un programa CLI usando el módulo Typer y distribuir su paquete a PyPI. Para profundizar más, aprendió a definir comandos y grupos de comandos, desarrollar una sesión CLI interactiva e incursionar en escenarios CLI comunes, como la interrupción del teclado.


Has sido un auténtico mago al aguantar todo. ¿No te unirás a mí en la Parte 3, donde implementamos las funcionalidades opcionales?