Ya hemos superado con creces el proyecto escolar básico de piedra, papel y tijera: profundicemos en él. ¿Qué conseguiremos con este tutorial? En , creamos con éxito la lógica empresarial para interactuar con el SDK de Trello. Cómo crear un programa CLI de Python para la administración de placas de Trello (Parte 1) Aquí hay un resumen rápido de la arquitectura de nuestro programa CLI: 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 . Esta vez queremos implementar una carpeta con módulos para diferentes funcionalidades, a saber: trelloservice cli 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 , lo almacenaremos en el archivo CLI principal ya que no pertenece a ningún grupo de comandos. list 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 ? Esto se usará para almacenar activos relacionados para nuestro , mientras que al haber una carpeta recientemente implementada en , la usaremos para almacenar módulos que se usarán en todo el software. assets README shared trellocli Configuración Comencemos modificando nuestro archivo de punto de entrada, . 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, , tiene una instancia que podemos ejecutar. __main__.py cli.py app # 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 ; Almacenaremos nuestra instancia aquí. La idea es inicializar un objeto para compartirlo con todo el software. cli.py app Typer # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer() Avanzando con este concepto, modifiquemos nuestro para especificar nuestros scripts de línea de comando. Aquí, proporcionaremos un nombre para nuestro paquete y definiremos el punto de entrada. pyproject.toml # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main" Según el ejemplo anterior, definimos como el nombre del paquete y la función en el script , que está almacenado en el módulo , se ejecutará durante el tiempo de ejecución. trellocli main __main__ trellocli Ahora que la parte CLI de nuestro software está configurada, modifiquemos nuestro módulo para servir mejor a nuestro programa CLI. Como recordará, nuestro módulo 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 . Esto asegurará que nuestro programa sea más limpio y más descriptivo en términos de instrucciones. trelloservice trelloservice config access Para poner esto en palabras, modificaremos estas funciones: __init__ __load_oauth_token_env_var authorize is_authorized Comenzando con la función , inicializaremos un cliente vacío en lugar de manejar la configuración del cliente aquí. __init__ # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None 💡 ¿Puedes modificar nuestra función 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. Rincón del desafío __load_oauth_token_env_var Pasando a las funciones auxiliares e , la idea es que llevará a cabo la lógica empresarial de configurar el cliente utilizando la función mientras que la función simplemente devuelve un valor booleano de si se concedió la autorización. authorize is_authorized authorize __load_oauth_token_env_var is_authorized # 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 y es que es una que sirve para almacenar el token de autorización como una variable de entorno, mientras que es la función pública, intenta recuperar todas las credenciales necesarias e inicializar un Cliente Trello. __load_oauth_token_env_var authorize __load_oauth_token_env_var función interna authorize 💡Observe cómo nuestra función devuelve un tipo de datos . ¿Puedes implementar un modelo que tenga el atributo ? Consulte (Sugerencia: observe cómo creamos modelos) Rincón del desafío authorize AuthorizeResponse status_code la Parte 1 de Cómo crear un programa CLI de Python para la administración de tableros de Trello Por último, creemos una instancia de un objeto singleton hacia la parte inferior del módulo. No dudes en consultar este parche para ver cómo se ve el código completo: TrelloService 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 definidos en nuestro inicializador, ya que estas excepciones son subclases de y actúan como excepciones típicas definidas por el usuario, mientras que los sirven más como valores constantes que comienzan desde 0. ERRORS BaseException ERRORS 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 para probar los resultados esperados. Lo bueno del módulo es que viene con su propio objeto . En cuanto a ejecutar las pruebas, las vincularemos con el módulo . Para obtener más información, consulte los . shell Typer runner pytest 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 , también conocido como 0. Aquí hay un excelente artículo de RedHat sobre . success 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 💡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) Rincón del desafío Lógica de negocios Módulo CLI principal Comprenda que este será nuestro módulo 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. cli En este módulo, almacenaremos nuestro comando . Profundizando en el comando, sabemos que queremos implementar las siguientes opciones: list board_name: requerido si no se configuró previamente config board 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 ) o simplemente usar una variable de entorno predeterminada. Sin embargo, para nuestro caso de uso, hagámoslo sencillo generando nuestra excepción personalizada si no se cumplen las condiciones. documentos oficiales InvalidUserInputError Para desarrollar el comando, comencemos definiendo las opciones. En Typer, como se menciona en sus , los ingredientes clave para definir una opción serían: documentos oficiales Tipo de datos Texto de ayuda Valor por defecto Por ejemplo, para crear la opción con las siguientes condiciones: detailed 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 con las opciones necesarias, trataremos como una función de Python y sus opciones como parámetros obligatorios. list list # 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 inicializada en la parte superior del archivo. Siéntete libre de navegar por el para modificar las Opciones a tu gusto. app código base oficial de Typer 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 . Al utilizar bloques , podemos evitar que nuestro programa sufra fallos fatales. SUCCESS try-catch Al configurar el tablero apropiado para usar, intentaremos configurar el tablero Trello para su uso según el recuperado. Por lo tanto, queremos cubrir los siguientes casos de uso. board_id Recuperar si se proporcionó explícitamente verificando una coincidencia usando la función en trellojob board_id board_name get_all_boards Recuperar el almacenado como una variable de entorno si no se usó la opción board_id board_name Los datos que mostraremos se formatearán utilizando la funcionalidad del paquete . Para obtener más información sobre , consulte sus Table rich rich 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 que aún tenemos que implementar. Consulte este parche si necesita ayuda para implementarlo: TrelloService 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 en la terminal. De forma predeterminada, el módulo Typer completará la salida del comando por sí solo. Y observe cómo podemos llamar como nombre del paquete. ¿Recuerda cómo se definió esto previamente en nuestro ? python -m trellocli --help --help trellocli pyproject.toml Avancemos un poco e inicialicemos también los grupos de comandos y . Para hacerlo, simplemente usaremos la función en nuestro objeto . La idea es que el grupo de comandos tenga su propio objeto , y simplemente lo agregaremos a la principal en , junto con el nombre del grupo de comandos y el texto de ayuda. Debería verse algo como esto create config add_typer app app app cli.py # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") 💡 ¿Podrías importar el grupo de comandos por tu cuenta? No dude en consultar este parche para obtener ayuda: Rincón del desafío create trello-cli-kit Subcomandos Para configurar un grupo de comandos para , almacenaremos sus respectivos comandos en su propio módulo. La configuración es similar a la de 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 , o en otras palabras, interrumpe el proceso. La razón por la que no cubrimos esto para nuestro comando es porque la diferencia aquí es que el grupo de comandos 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. create cli.py Ctrl + C list config Comenzando con el comando , finalmente usaremos la función creada en nuestro . Dado que la función maneja la configuración por sí sola, solo tendremos que verificar la ejecución del proceso. access authorize TrelloService authorize # 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 , utilizaremos varios módulos para brindar una buena experiencia de usuario, incluido para mostrar una GUI de terminal para la interacción del usuario. La idea principal es la siguiente: board el Menú de terminal simple 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 hasta recuperar datos del tablero. list 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 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 . También es importante tener en cuenta que debemos manejar adecuadamente los valores opcionales: Simple Terminal Menu rich 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...") 💡¿Puedes mostrar una barra de progreso para el proceso ? Sugerencia: eche un vistazo al uso de Rincón del desafío add de la función de estado 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 es especificar qué módulo almacena el paquete en sí. En nuestro caso, será . Aquí están los metadatos para agregar: pyproject.toml trellocli # pyproject.toml [tool.setuptools] packages = ["trellocli"] En cuanto a nuestro , 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 , debe usar su URL absoluta, que generalmente tiene el siguiente formato README.md README.md https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image> PruebaPyPI Usaremos las herramientas y para crear y publicar nuestro paquete. Ejecute el siguiente comando en su terminal para crear un archivo fuente y una rueda para su paquete: build twine 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: ). Simplemente ingrese los siguientes valores: enlace a la documentación 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 en tu flujo de trabajo de GitHub y selecciona un nuevo flujo de trabajo. Usaremos el flujo de trabajo 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). Actions Publish Python Package 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 , y . A continuación, cree una etiqueta para su última confirmación ejecutando el siguiente comando (para obtener más información sobre las etiquetas: ) pull add commit 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 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. Karol Horosin sobre la integración de CI/CD con su paquete Python Envolver Este fue largo 😓. A través de este tutorial, aprendió a transformar su software en un programa CLI usando el módulo 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. Typer Has sido un auténtico mago al aguantar todo. ¿No te unirás a mí en la Parte 3, donde implementamos las funcionalidades opcionales?