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 , nous avons réussi à créer la logique métier pour interagir avec le SDK Trello. Comment créer un programme Python CLI pour Trello Board Management (Partie 1) Voici un bref récapitulatif de l’architecture de notre programme CLI : 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 . Cette fois-ci, nous souhaitons implémenter un dossier avec des modules pour différentes fonctionnalités, à savoir : trelloservice cli 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 , nous la stockerons dans le fichier CLI principal car elle n'appartient à aucun groupe de commandes. list 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 ? Ceci sera utilisé pour stocker les actifs associés à notre , alors qu'il existe un dossier nouvellement implémenté dans , nous l'utiliserons pour stocker les modules à utiliser dans le logiciel. assets README shared trellocli Installation Commençons par modifier notre fichier de point d'entrée, . 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, , dispose d'une instance que nous pouvons exécuter. __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 rapide vers notre fichier ; nous allons stocker notre instance ici. L'idée est d'initialiser un objet à partager dans le logiciel. cli.py app Typer # 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 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 # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main" Sur la base de l'exemple ci-dessus, nous avons défini comme nom du package et la fonction du script , qui est stockée dans le module , sera exécutée pendant l'exécution. trellocli main __main__ trellocli Maintenant que la partie CLI de notre logiciel est configurée, modifions notre module pour mieux servir notre programme CLI. Comme vous vous en souvenez, notre module 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 . Cela garantira que notre programme est plus propre et plus descriptif en termes d’instructions. trelloservice trelloservice config access 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 , nous allons initialiser un client vide au lieu de gérer la configuration du client ici. __init__ # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None 💡Pouvez-vous modifier notre fonction 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. Coin des défis __load_oauth_token_env_var Passant aux fonctions d'assistance et , l'idée est exécutera la logique métier de configuration du client en utilisant la fonction tandis que la fonction renvoie simplement une valeur booléenne indiquant si l'autorisation a été accordée. 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 Comprenez que la différence entre et est que est une qui sert à stocker le jeton d'autorisation en tant que variable d'environnement alors étant la fonction publique, elle tente de récupérer toutes les informations d'identification nécessaires et d'initialiser un client Trello. __load_oauth_token_env_var authorize __load_oauth_token_env_var fonction interne authorize 💡Remarquez comment notre fonction renvoie un type de données . Pouvez-vous implémenter un modèle doté de l'attribut ? Reportez-vous à (Indice : regardez comment nous avons créé des modèles) Coin des défis authorize AuthorizeResponse status_code la première partie de Comment créer un programme Python CLI pour Trello Board Management Enfin, instancions un objet singleton vers le bas du module. N'hésitez pas à vous référer à ce patch pour voir à quoi ressemble le code complet : TrelloService 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 définies dans notre initialiseur car ces exceptions sont des sous-classes de et agissent comme des exceptions typiques définies par l'utilisateur, tandis que les servent davantage de valeurs constantes à partir de 0. ERRORS BaseException ERRORS 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 pour tester les résultats attendus. Ce qui est génial avec le module , c'est qu'il est livré avec son propre objet . Quant à l'exécution des tests, nous l'associerons au module . Pour plus d'informations, consultez la . shell Typer runner pytest 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 , c'est-à-dire 0. Voici un excellent article de RedHat sur . success 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 💡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) Coin des défis Logique métier Module CLI principal Comprenez qu'il s'agira de notre module 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é. cli Dans ce module, nous stockerons notre commande . En approfondissant la commande, nous savons que nous souhaitons implémenter les options suivantes : list board_name : requis si n'a pas été définie précédemment config board 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 ) 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 si les conditions ne sont pas remplies. documentation officielle InvalidUserInputError Pour créer la commande, commençons par définir les options. Dans Typer, comme mentionné dans leur , les ingrédients clés pour définir une option seraient : documentation officielle Type de données Texte d'aide Valeur par défaut Par exemple, pour créer l'option avec les conditions suivantes : detailed 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 avec les options nécessaires, nous traiterons comme une fonction Python et ses options comme paramètres requis. 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 Notez que nous ajoutons la commande à l'instance initialisée vers le haut du fichier. N'hésitez pas à naviguer dans la pour modifier les options à votre guise. app base de code officielle de Typer 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 a été fournie) board_name 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 a été sélectionnée) detailed Quelques points à noter… Nous souhaitons déclencher des exceptions lorsque le trellojob produit un code d'état autre que . En utilisant des blocs , nous pouvons empêcher notre programme de crashs fatals. SUCCESS try-catch Lors de la configuration du tableau approprié à utiliser, nous tenterons de configurer le tableau Trello pour une utilisation en fonction du récupéré. Ainsi, nous souhaitons couvrir les cas d'utilisation suivants board_id Récupérer le si le a été explicitement fourni en vérifiant une correspondance à l'aide de la fonction dans le trellojob board_id board_name get_all_boards Récupération du tel qu'il est stocké en tant que variable d'environnement si l'option n'a pas été utilisée board_id board_name Les données que nous afficherons seront formatées à l'aide de la fonctionnalité du package . Pour plus d'informations sur , veuillez vous référer à leurs Table rich rich 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 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 : 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...") Pour voir notre logiciel en action, exécutez simplement dans le terminal. Par défaut, le module Typer remplira lui-même la sortie de la commande . Et remarquez comment nous pouvons appeler comme nom de package - rappelez-vous comment cela a été précédemment défini dans notre ? python -m trellocli --help --help trellocli pyproject.toml Avançons un peu et initialisons également les groupes de commandes et . Pour ce faire, nous utiliserons simplement la fonction sur notre objet . L'idée est que le groupe de commandes aura son propre objet , et nous l'ajouterons simplement dans l' principale dans , avec le nom du groupe de commandes et le texte d'assistance. Ça devrait ressembler a quelque chose comme ca 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") 💡Pourriez-vous importer le groupe de commandes par vous-même ? N'hésitez pas à vous référer à ce patch pour obtenir de l'aide : Coin des défis create trello-cli-kit Sous-commandes Pour configurer un groupe de commandes pour , nous stockerons ses commandes respectives dans son propre module. La configuration est similaire à celle de 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 , ou en d'autres termes, interrompt le processus. La raison pour laquelle nous n'avons pas abordé ce point pour notre commande est que la différence ici est que le groupe de commandes 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. create cli.py Ctrl + C list config En commençant par la commande , nous utiliserons enfin la fonction telle que créée dans notre . Puisque la fonction gère seule la configuration, nous n'aurons qu'à vérifier l'exécution du processus. 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 ce qui concerne la commande , nous utiliserons divers modules pour offrir une bonne expérience utilisateur, notamment pour afficher une interface graphique de terminal pour l'interaction de l'utilisateur. L'idée principale est la suivante : board un menu de terminal simple 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 jusqu'à la récupération des données du tableau. list 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 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 . Il est également important de noter que nous devons gérer correctement les valeurs facultatives : Simple Terminal Menu rich 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...") 💡Pouvez-vous afficher une barre de progression pour le processus ? Astuce : jetez un œil à l'utilisation de Coin des défis add de la fonctionnalité de statut 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 est de spécifier quel module stocke le package lui-même. Dans notre cas, ce sera . Voici les métadonnées à ajouter : pyproject.toml trellocli # pyproject.toml [tool.setuptools] packages = ["trellocli"] Quant à notre , 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 , vous devez utiliser son URL absolue, qui est généralement du format suivant README.md README.md https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image> TestPyPI Nous utiliserons les outils et 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 : build twine 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 : ). Mettez simplement les valeurs suivantes : lien vers la documentation 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 de votre workflow GitHub et sélectionnez un nouveau workflow. Nous utiliserons le workflow 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). Actions Publish Python Package 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 , et . Ensuite, créez une balise pour votre dernier commit en exécutant la commande suivante (Pour plus d'informations sur les balises : ) pull add commit 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 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. Karol Horosin sur l'intégration de CI/CD à votre package Python Conclure Ce fut long 😓. Grâce à ce tutoriel, vous avez appris à transformer votre logiciel en programme CLI à l'aide du module 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. Typer 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 ?