paint-brush
Comment créer un programme Python CLI pour Trello Board Management (Partie 2)par@elainechan01
1,672 lectures
1,672 lectures

Comment créer un programme Python CLI pour Trello Board Management (Partie 2)

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

Trop long; Pour lire

Partie 2 de la série de didacticiels sur la création d'un programme CLI Python pour Trello Board Management, axée sur la façon d'écrire une logique métier pour les commandes CLI et la distribution de packages Python.
featured image - Comment créer un programme Python CLI pour Trello Board Management (Partie 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

Nous avons désormais bien dépassé le projet d'école de base pierre-papier-ciseaux - plongeons-y directement.


Qu’allons-nous réaliser grâce à ce tutoriel ?

Dans Comment créer un programme Python CLI pour Trello Board Management (Partie 1) , nous avons réussi à créer la logique métier pour interagir avec le SDK Trello.


Voici un bref récapitulatif de l’architecture de notre programme CLI :

Vue détaillée de la structure CLI en fonction des exigences


Dans ce didacticiel, nous verrons comment transformer notre projet en programme CLI, en nous concentrant sur les exigences fonctionnelles et non fonctionnelles.


D'un autre côté, nous apprendrons également comment distribuer notre programme sous forme de package sur PyPI .


Commençons


Structure des dossiers

Auparavant, nous avions réussi à mettre en place un squelette pour héberger notre module trelloservice . Cette fois-ci, nous souhaitons implémenter un dossier cli avec des modules pour différentes fonctionnalités, à savoir :


  • configuration
  • accéder
  • liste


L'idée est que, pour chaque groupe de commandes, ses commandes seront stockées dans son propre module. Quant à la commande list , nous la stockerons dans le fichier CLI principal car elle n'appartient à aucun groupe de commandes.


D'un autre côté, regardons comment nettoyer notre structure de dossiers. Plus précisément, nous devrions commencer à tenir compte de l'évolutivité du logiciel en veillant à ce que les répertoires ne soient pas encombrés.


Voici une suggestion sur la structure de nos dossiers :

 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


Remarquez à quoi ressemble le dossier assets ? Ceci sera utilisé pour stocker les actifs associés à notre README , alors qu'il existe un dossier shared nouvellement implémenté dans trellocli , nous l'utiliserons pour stocker les modules à utiliser dans le logiciel.


Installation

Commençons par modifier notre fichier de point d'entrée, __main__.py . En ce qui concerne l'importation elle-même, comme nous avons décidé de stocker les modules associés dans leurs propres sous-dossiers, nous devrons prendre en compte ces modifications. D'un autre côté, nous supposons également que le module CLI principal, cli.py , dispose d'une instance app que nous pouvons exécuter.

 # 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 rapide vers notre fichier cli.py ; nous allons stocker notre instance app ici. L'idée est d'initialiser un objet Typer à partager dans le logiciel.

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


Pour aller de l'avant avec ce concept, modifions notre pyproject.toml pour spécifier nos scripts de ligne de commande. Ici, nous allons donner un nom à notre package et définir le point d'entrée.

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


Sur la base de l'exemple ci-dessus, nous avons défini trellocli comme nom du package et la fonction main du script __main__ , qui est stockée dans le module trellocli , sera exécutée pendant l'exécution.


Maintenant que la partie CLI de notre logiciel est configurée, modifions notre module trelloservice pour mieux servir notre programme CLI. Comme vous vous en souvenez, notre module trelloservice est configuré pour demander récursivement l'autorisation de l'utilisateur jusqu'à ce qu'elle soit approuvée. Nous allons modifier cela de manière à ce que le programme se ferme si l'autorisation n'est pas donnée et inciter l'utilisateur à exécuter la commande config access . Cela garantira que notre programme est plus propre et plus descriptif en termes d’instructions.


Pour mettre cela en mots, nous allons modifier ces fonctions :


  • __init__
  • __load_oauth_token_env_var
  • authorize
  • is_authorized


En commençant par la fonction __init__ , nous allons initialiser un client vide au lieu de gérer la configuration du client ici.

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


Coin des défis 💡Pouvez-vous modifier notre fonction __load_oauth_token_env_var afin qu'elle ne demande pas de manière récursive l'autorisation de l'utilisateur ? Astuce : Une fonction récursive est une fonction qui s’appelle elle-même.


Passant aux fonctions d'assistance authorize et is_authorized , l'idée est authorize exécutera la logique métier de configuration du client en utilisant la fonction __load_oauth_token_env_var tandis que la fonction is_authorized renvoie simplement une valeur booléenne indiquant si l'autorisation a été accordée.

 # 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


Comprenez que la différence entre __load_oauth_token_env_var et authorize est que __load_oauth_token_env_var est une fonction interne qui sert à stocker le jeton d'autorisation en tant que variable d'environnement alors authorize étant la fonction publique, elle tente de récupérer toutes les informations d'identification nécessaires et d'initialiser un client Trello.


Coin des défis 💡Remarquez comment notre fonction authorize renvoie un type de données AuthorizeResponse . Pouvez-vous implémenter un modèle doté de l'attribut status_code ? Reportez-vous à la première partie de Comment créer un programme Python CLI pour Trello Board Management (Indice : regardez comment nous avons créé des modèles)


Enfin, instancions un objet TrelloService singleton vers le bas du module. N'hésitez pas à vous référer à ce patch pour voir à quoi ressemble le code complet : trello-cli-kit

 # trellocli/trelloservice.py trellojob = TrelloService()


Enfin, nous souhaitons initialiser certaines exceptions personnalisées à partager dans l'ensemble du programme. Ceci est différent des ERRORS définies dans notre initialiseur car ces exceptions sont des sous-classes de BaseException et agissent comme des exceptions typiques définies par l'utilisateur, tandis que les ERRORS servent davantage de valeurs constantes à partir de 0.


Gardons nos exceptions au minimum et retenons certains des cas d'utilisation courants, notamment :


  • Erreur de lecture : déclenchée en cas d'erreur de lecture depuis Trello
  • Erreur d'écriture : déclenchée en cas d'erreur d'écriture sur Trello
  • Erreur d'autorisation : déclenchée lorsque l'autorisation n'est pas accordée pour Trello
  • Erreur d'entrée utilisateur non valide : déclenchée lorsque l'entrée CLI de l'utilisateur n'est pas reconnue


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


Tests unitaires

Comme mentionné dans la première partie, nous n'aborderons pas en détail les tests unitaires dans ce didacticiel, travaillons donc uniquement avec les éléments nécessaires :


  • Test pour configurer l'accès
  • Test pour configurer le tableau trello
  • Test pour créer une nouvelle carte Trello
  • Test pour afficher les détails du tableau Trello
  • Test pour afficher les détails du tableau Trello (vue détaillée)


L'idée est de se moquer d'un interpréteur de ligne de commande, comme un shell pour tester les résultats attendus. Ce qui est génial avec le module Typer , c'est qu'il est livré avec son propre objet runner . Quant à l'exécution des tests, nous l'associerons au module pytest . Pour plus d'informations, consultez la documentation officielle de Typer .


Travaillons ensemble sur le premier test, c'est-à-dire configurer l'accès. Comprenez que nous testons si la fonction s'exécute correctement. Pour ce faire, nous vérifierons la réponse du système et si le code de sortie est success , c'est-à-dire 0. Voici un excellent article de RedHat sur ce que sont les codes de sortie et comment le système les utilise pour communiquer les processus .

 # 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


Coin des défis 💡Maintenant que vous avez compris l'essentiel, pouvez-vous implémenter d'autres cas de test par vous-même ? (Indice : vous devriez également envisager de tester les cas d'échec)


Logique métier


Module CLI principal

Comprenez qu'il s'agira de notre module cli principal - pour tous les groupes de commandes (config, create), leur logique métier sera stockée dans son propre fichier séparé pour une meilleure lisibilité.


Dans ce module, nous stockerons notre commande list . En approfondissant la commande, nous savons que nous souhaitons implémenter les options suivantes :


  • board_name : requis si config board n'a pas été définie précédemment
  • détaillé : affichage dans une vue détaillée


En commençant par l'option requise board_name, il existe plusieurs façons d'y parvenir, l'une d'entre elles étant d'utiliser la fonction de rappel (pour plus d'informations, voici la documentation officielle ) ou simplement d'utiliser une variable d'environnement par défaut. Cependant, pour notre cas d'utilisation, restons simples en déclenchant notre exception personnalisée InvalidUserInputError si les conditions ne sont pas remplies.


Pour créer la commande, commençons par définir les options. Dans Typer, comme mentionné dans leur documentation officielle , les ingrédients clés pour définir une option seraient :


  • Type de données
  • Texte d'aide
  • Valeur par défaut


Par exemple, pour créer l'option detailed avec les conditions suivantes :


  • Type de données : booléen
  • Texte d'aide : « Activer la vue détaillée »
  • Valeur par défaut : Aucun


Notre code ressemblerait à ceci :

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


Dans l'ensemble, pour définir la commande list avec les options nécessaires, nous traiterons list comme une fonction Python et ses options comme paramètres requis.

 # 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


Notez que nous ajoutons la commande à l'instance app initialisée vers le haut du fichier. N'hésitez pas à naviguer dans la base de code officielle de Typer pour modifier les options à votre guise.


Quant au workflow de la commande, nous optons pour quelque chose comme ceci :


  • Vérifier l'autorisation
  • Configurez pour utiliser la carte appropriée (vérifiez si l'option board_name a été fournie)
  • Configurer le tableau Trello pour qu'il soit lu
  • Récupérez les données de la carte Trello appropriées et catégorisez en fonction de la liste Trello
  • Afficher les données (vérifier si l'option detailed a été sélectionnée)


Quelques points à noter…


  • Nous souhaitons déclencher des exceptions lorsque le trellojob produit un code d'état autre que SUCCESS . En utilisant des blocs try-catch , nous pouvons empêcher notre programme de crashs fatals.
  • Lors de la configuration du tableau approprié à utiliser, nous tenterons de configurer le tableau Trello pour une utilisation en fonction du board_id récupéré. Ainsi, nous souhaitons couvrir les cas d'utilisation suivants
    • Récupérer le board_id si le board_name a été explicitement fourni en vérifiant une correspondance à l'aide de la fonction get_all_boards dans le trellojob
    • Récupération du board_id tel qu'il est stocké en tant que variable d'environnement si l'option board_name n'a pas été utilisée
  • Les données que nous afficherons seront formatées à l'aide de la fonctionnalité Table du package rich . Pour plus d'informations sur rich , veuillez vous référer à leurs documents officiels
    • Détaillé : affichez un résumé du nombre de listes Trello, du nombre de cartes et des étiquettes définies. Pour chaque liste Trello, affichez toutes les cartes et leur nom correspondant, leurs descriptions et les étiquettes associées
    • Non détaillé : Afficher un récapitulatif du nombre de listes Trello, du nombre de cartes et des étiquettes définies


En mettant le tout ensemble, nous obtenons quelque chose comme suit. Avertissement : il se peut que TrelloService comporte certaines fonctions manquantes que nous n'avons pas encore implémentées. Veuillez vous référer à ce patch si vous avez besoin d'aide pour les implémenter : 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...")


Pour voir notre logiciel en action, exécutez simplement python -m trellocli --help dans le terminal. Par défaut, le module Typer remplira lui-même la sortie de la commande --help . Et remarquez comment nous pouvons appeler trellocli comme nom de package - rappelez-vous comment cela a été précédemment défini dans notre pyproject.toml ?


Avançons un peu et initialisons également les groupes de commandes create et config . Pour ce faire, nous utiliserons simplement la fonction add_typer sur notre objet app . L'idée est que le groupe de commandes aura son propre objet app , et nous l'ajouterons simplement dans l' app principale dans cli.py , avec le nom du groupe de commandes et le texte d'assistance. Ça devrait ressembler a quelque chose comme ca

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


Coin des défis 💡Pourriez-vous importer le groupe de commandes create par vous-même ? N'hésitez pas à vous référer à ce patch pour obtenir de l'aide : trello-cli-kit


Sous-commandes

Pour configurer un groupe de commandes pour create , nous stockerons ses commandes respectives dans son propre module. La configuration est similaire à celle de cli.py avec la nécessité d'instancier un objet Typer. En ce qui concerne les commandes, nous souhaitons également respecter la nécessité d'utiliser des exceptions personnalisées. Un sujet supplémentaire que nous souhaitons aborder concerne le moment où l'utilisateur appuie sur Ctrl + C , ou en d'autres termes, interrompt le processus. La raison pour laquelle nous n'avons pas abordé ce point pour notre commande list est que la différence ici est que le groupe de commandes config est constitué de commandes interactives. La principale différence entre les commandes interactives est qu'elles nécessitent une interaction continue de l'utilisateur. Bien sûr, disons que notre commande directe prend beaucoup de temps à s'exécuter. Il est également recommandé de gérer les interruptions potentielles du clavier.


En commençant par la commande access , nous utiliserons enfin la fonction authorize telle que créée dans notre TrelloService . Puisque la fonction authorize gère seule la configuration, nous n'aurons qu'à vérifier l'exécution du processus.

 # 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 ce qui concerne la commande board , nous utiliserons divers modules pour offrir une bonne expérience utilisateur, notamment un menu de terminal simple pour afficher une interface graphique de terminal pour l'interaction de l'utilisateur. L'idée principale est la suivante :


  • Vérifier l'autorisation
  • Récupérer tous les tableaux Trello du compte de l'utilisateur
  • Afficher un menu de terminal à sélection unique de tableaux Trello
  • Définir l'ID du tableau Trello sélectionné comme variable d'environnement


 # 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...")


Enfin, nous abordons l'exigence fonctionnelle principale de notre logiciel : ajouter une nouvelle carte à une liste sur le tableau Trello. Nous utiliserons les mêmes étapes depuis notre commande list jusqu'à la récupération des données du tableau.


En dehors de cela, nous demanderons de manière interactive la saisie de l'utilisateur pour configurer correctement la nouvelle carte :


  • Liste Trello à ajouter à : Sélection unique
  • Nom de la carte : Texte
  • [Facultatif] Description de la carte : texte
  • [Facultatif] Libellés : sélection multiple
  • Confirmation : o/N


Pour toutes les invites nécessitant que l'utilisateur sélectionne dans une liste, nous utiliserons le package Simple Terminal Menu comme auparavant. En ce qui concerne les autres invites et éléments divers comme la nécessité de saisir du texte ou la confirmation de l'utilisateur, nous utiliserons simplement le package rich . Il est également important de noter que nous devons gérer correctement les valeurs facultatives :


  • Les utilisateurs peuvent ignorer la fourniture d'une description
  • Les utilisateurs peuvent fournir une sélection vide pour les étiquettes


 # 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...")


Coin des défis 💡Pouvez-vous afficher une barre de progression pour le processus add ? Astuce : jetez un œil à l'utilisation de la fonctionnalité de statut de rich


Distribution des colis

Voici la partie amusante : distribuer officiellement notre logiciel sur PyPI. Pour ce faire, nous suivrons ce pipeline :


  • Configurer les métadonnées + mettre à jour le README
  • Télécharger pour tester PyPI
  • Configurer les actions GitHub
  • Pousser le code vers Tag v1.0.0
  • Distribuez le code à PyPI 🎉


Pour une explication détaillée, consultez cet excellent tutoriel sur Python Packaging de Ramit Mittal.


Configuration des métadonnées

Le dernier détail dont nous avons besoin pour notre pyproject.toml est de spécifier quel module stocke le package lui-même. Dans notre cas, ce sera trellocli . Voici les métadonnées à ajouter :

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


Quant à notre README.md , c'est une bonne pratique de fournir une sorte de guide, qu'il s'agisse de directives d'utilisation ou de comment commencer. Si vous avez inclus des images dans votre README.md , vous devez utiliser son URL absolue, qui est généralement du format suivant

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


TestPyPI

Nous utiliserons les outils build et twine pour créer et publier notre package. Exécutez la commande suivante dans votre terminal pour créer une archive source et une roue pour votre package :

 python -m build


Assurez-vous que vous disposez déjà d'un compte configuré sur TestPyPI et exécutez la commande suivante

 twine upload -r testpypi dist/*


Vous serez invité à saisir votre nom d'utilisateur et votre mot de passe. En raison de l'activation de l'authentification à deux facteurs, vous devrez utiliser un jeton API (pour plus d'informations sur la façon d'acquérir un jeton API TestPyPI : lien vers la documentation ). Mettez simplement les valeurs suivantes :


  • nom d'utilisateur: jeton
  • mot de passe : <votre jeton TestPyPI>


Une fois cette opération terminée, vous devriez pouvoir vous rendre sur TestPyPI pour vérifier votre package nouvellement distribué !


Configuration de GitHub

L'objectif est d'utiliser GitHub comme moyen de mettre à jour en permanence les nouvelles versions de votre package en fonction des balises.


Tout d’abord, rendez-vous sur l’onglet Actions de votre workflow GitHub et sélectionnez un nouveau workflow. Nous utiliserons le workflow Publish Python Package créé par GitHub Actions. Remarquez comment le flux de travail nécessite la lecture des secrets du référentiel ? Assurez-vous que vous avez stocké votre jeton PyPI sous le nom spécifié (l'acquisition d'un jeton API PyPI est similaire à celle de TestPyPI).


Une fois le workflow créé, nous pousserons notre code vers la balise v1.0.0. Pour plus d'informations sur la syntaxe de dénomination des versions, voici une excellente explication de Py-Pkgs : lien vers la documentation


Exécutez simplement les commandes habituelles pull , add et commit . Ensuite, créez une balise pour votre dernier commit en exécutant la commande suivante (Pour plus d'informations sur les balises : lien vers la documentation )

 git tag <tagname> HEAD


Enfin, transférez votre nouvelle balise vers le référentiel distant

 git push <remote name> <tag name>


Voici un excellent article de Karol Horosin sur l'intégration de CI/CD à votre package Python si vous souhaitez en savoir plus. Mais pour l'instant, asseyez-vous et profitez de votre dernière réalisation 🎉. N'hésitez pas à regarder la magie s'opérer en tant que workflow GitHub Actions lors de la distribution de votre package à PyPI.


Conclure

Ce fut long 😓. Grâce à ce tutoriel, vous avez appris à transformer votre logiciel en programme CLI à l'aide du module Typer et à distribuer votre package sur PyPI. Pour approfondir, vous avez appris à définir des commandes et des groupes de commandes, à développer une session CLI interactive et à vous familiariser avec des scénarios CLI courants tels que l'interruption du clavier.


Vous avez été un magicien absolu pour avoir enduré tout cela. Ne me rejoignez-vous pas pour la partie 3, où nous implémentons les fonctionnalités optionnelles ?