Nous avons désormais bien dépassé le projet d'école de base pierre-papier-ciseaux - plongeons-y directement.
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 :
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 .
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 :
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.
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 :
# trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass
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 :
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)
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 :
config board
n'a pas été définie précédemment
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 :
Par exemple, pour créer l'option detailed
avec les conditions suivantes :
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 :
board_name
a été fournie)detailed
a été sélectionnée)
Quelques points à noter…
SUCCESS
. En utilisant des blocs try-catch
, nous pouvons empêcher notre programme de crashs fatals.board_id
récupéré. Ainsi, nous souhaitons couvrir les cas d'utilisation suivantsboard_id
si le board_name
a été explicitement fourni en vérifiant une correspondance à l'aide de la fonction get_all_boards
dans le trellojobboard_id
tel qu'il est stocké en tant que variable d'environnement si l'option board_name
n'a pas été utiliséeTable
du package rich
. Pour plus d'informations sur rich
, veuillez vous référer à leurs documents officiels
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
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 :
# 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 :
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 :
# 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
Voici la partie amusante : distribuer officiellement notre logiciel sur PyPI. Pour ce faire, nous suivrons ce pipeline :
Pour une explication détaillée, consultez cet excellent tutoriel sur Python Packaging de Ramit Mittal.
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>
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 :
Une fois cette opération terminée, vous devriez pouvoir vous rendre sur TestPyPI pour vérifier votre package nouvellement distribué !
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.
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 ?