Ya hemos superado con creces el proyecto escolar básico de piedra, papel y tijera: profundicemos en él.
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:
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 .
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:
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.
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:
# trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass
Como se mencionó en la Parte I, no cubriremos extensamente las pruebas unitarias en este tutorial, así que trabajemos solo con los elementos necesarios:
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)
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:
config board
no se configuró previamente
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:
Por ejemplo, para crear la opción detailed
con las siguientes condiciones:
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:
board_name
)detailed
)
Algunas cosas a tener en cuenta...
SUCCESS
. Al utilizar bloques try-catch
, podemos evitar que nuestro programa sufra fallos fatales.board_id
recuperado. Por lo tanto, queremos cubrir los siguientes casos de uso.board_id
si board_name
se proporcionó explícitamente verificando una coincidencia usando la función get_all_boards
en trellojobboard_id
almacenado como una variable de entorno si no se usó la opción board_name
Table
del paquete rich
. Para obtener más información sobre rich
, consulte sus documentos oficiales.
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
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:
# 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:
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:
# 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
Aquí viene la parte divertida: distribuir oficialmente nuestro software en PyPI. Seguiremos este canal para hacerlo:
Para obtener una explicación detallada, consulte este excelente tutorial sobre Python Packaging de Ramit Mittal.
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>
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:
Una vez que se haya completado, debería poder dirigirse a TestPyPI para ver su paquete recién distribuido.
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.
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?