Salut, jouons ! Choisissez votre combattant (pierre, papier, ciseaux) : pierre
De nombreux logiciels courants sont intégrés dans un programme CLI. Prenez l'éditeur de texte vim
par exemple - un outil fourni avec n'importe quel système UNIX qui peut être activé simplement en exécutant vim <FILE>
dans le terminal.
Par exemple, lorsque nous voulons définir la propriété project
dans la section principale, nous exécutons gcloud config set project <PROJECT_ID>
Argument | Contenu |
---|---|
Arg 0 | gcloud |
Arg 1 | configuration |
… | … |
Sur la base de l'exemple précédent, nous définissons la propriété project
dans la section principale en exécutant gcloud config set project <PROJECT_ID>
En d'autres termes, set
est une commande.
En se référant à la commande gcloud config
, comme indiqué dans leur documentation officielle, gcloud config
est un groupe de commandes qui vous permet de modifier les propriétés. L'utilisation est telle que :
gcloud config GROUP | COMMAND [GCLOUD_WIDE_FLAG … ]
où COMMAND peut être soit set
, list
, etc. (Notez que GROUP est config
)
Pour en revenir à l'utilisation du groupe de commandes gcloud config
, la ou les options, dans ce cas, sont GCLOUD_WIDE_FLAG
.
Par exemple, disons que nous voulions afficher l'utilisation détaillée et la description de la commande, nous exécutons gcloud config set –help
. En d'autres termes, --help
est l'option.
Un autre exemple est lorsque nous voulons définir la propriété de zone dans la section de calcul d'un projet spécifique, nous exécutons gcloud config set compute <ZONE_NAME> –project=<PROJECT_ID>
. En d'autres termes, --project
est une option qui contient la valeur <PROJECT_ID>
.
Par exemple, lorsque nous voulons créer un cluster dataproc, nous exécutons gcloud dataproc clusters create <CLUSTER_NAME> –region=<REGION>
. Et comme indiqué dans leur documentation d'utilisation :
gcloud dataproc clusters create (CLUSTER: –region=REGION)
L'indicateur --region
est obligatoire s'il n'a pas été configuré précédemment.
Les options courtes commencent par -
suivi d'un seul caractère alphanumérique, tandis que les options longues commencent par --
suivi de plusieurs caractères. Considérez les options courtes comme des raccourcis lorsque l'utilisateur est sûr de ce qu'il veut, tandis que les options longues sont plus lisibles.
Vous avez choisi le rock ! L'ordinateur va maintenant faire sa sélection.
Votre équipe utilise Trello pour suivre les problèmes et l'avancement du projet. Votre équipe recherche un moyen plus simplifié d'interagir avec le tableau - quelque chose de similaire à la création d'un nouveau référentiel GitHub via le terminal. L'équipe s'est tournée vers vous pour créer un programme CLI avec cette exigence de base de pouvoir ajouter une nouvelle carte à la colonne 'To Do' du tableau.
Exigences fonctionnelles
Prérogatives non fonctionnelles
Exigences facultatives
Ps Ne vous inquiétez pas pour les deux dernières colonnes, nous en apprendrons plus tard…
Tests unitaires
Trello
CLI
Utilitaires (Divers)
Partie 1
py-trello
Partie 2
Partie 3
L'ordinateur a choisi les ciseaux ! Voyons qui gagnera cette bataille...
trellocli/ __init__.py __main__.py models.py cli.py trelloservice.py tests/ test_cli.py test_trelloservice.py README.md pyproject.toml .env .gitignore
trellocli
: agit comme le nom du package à utiliser par les utilisateurs, par exemple, pip install trellocli
__init__.py
: représente la racine du package, conforme le dossier en tant que package Python__main__.py
: définit le point d'entrée et permet aux utilisateurs d'exécuter des modules sans spécifier le chemin du fichier en utilisant l'indicateur -m
, par exemple, python -m <module_name>
pour remplacer python -m <parent_folder>/<module_name>.py
models.py
: stocke les classes utilisées globalement, par exemple, les modèles auxquels les réponses de l'API sont censées se conformercli.py
: stocke la logique métier pour les commandes et options CLItrelloservice.py
: stocke la logique métier pour interagir avec py-trello
tests
: stocke les tests unitaires du programmetest_cli.py
: stocke les tests unitaires pour l'implémentation de la CLItest_trelloservice.py
: stocke les tests unitaires pour l'interaction avec py-trello
README.md
: stocke la documentation du programmepyproject.toml
: stocke les configurations et les exigences du package.env
: stocke les variables d'environnement.gitignore
: spécifie les fichiers à ignorer (non suivis) lors du contrôle de version
En commençant par le fichier __init__.py
dans notre package, qui serait l'endroit où les constantes et les variables du package sont stockées, telles que le nom et la version de l'application. Dans notre cas, nous souhaitons initialiser les éléments suivants :
# trellocli/__init__.py __app_name__ = "trellocli" __version__ = "0.1.0" ( SUCCESS, TRELLO_WRITE_ERROR, TRELLO_READ_ERROR ) = range(3) ERRORS = { TRELLO_WRITE_ERROR: "Error when writing to Trello", TRELLO_READ_ERROR: "Error when reading from Trello" }
Passant au fichier __main__.py
, le flux principal de votre programme doit être stocké ici. Dans notre cas, nous allons stocker le point d'entrée du programme CLI, en supposant qu'il y aura une fonction appelable dans cli.py
.
# trellocli/__main__.py from trellocli import cli def main(): # we'll modify this later - after the implementation of `cli.py` pass if __name__ == "__main__": main()
Maintenant que le package est configuré, intéressons-nous à la mise à jour de notre fichier README.md
(documentation principale). Il n'y a pas de structure spécifique que nous devons suivre, mais un bon README consisterait en ce qui suit :
<!--- README.md --> # Overview # Getting Started # Usage # Architecture ## Data Flow ## Tech Stack # Running Tests # Next Steps # References
# pyproject.toml [project] name = "trellocli_<YOUR_USERNAME>" version = "0.1.0" authors = [ { name = "<YOUR_NAME>", email = "<YOUR_EMAIL>" } ] description = "Program to modify your Trello boards from your computer's command line" readme = "README.md" requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] dependencies = [] [project.urls] "Homepage" = ""
Le prochain sur la liste serait notre fichier .env
où nous stockons nos variables d'environnement telles que les secrets et les clés de l'API. Il est important de noter que ce fichier ne doit pas être suivi par Git car il contient des informations sensibles.
Dans notre cas, nous allons stocker nos informations d'identification Trello ici. Pour créer un Power-Up dans Trello, suivez . Plus précisément, en fonction de l'utilisation par py-trello
, comme nous avons l'intention d'utiliser OAuth pour notre application, nous aurons besoin des éléments suivants pour interagir avec Trello :
Une fois que vous avez récupéré votre clé API et votre secret, stockez-les dans le fichier .env
en tant que tels
# .env TRELLO_API_KEY=<your_api_key> TRELLO_API_SECRET=<your_api_secret>
Enfin, utilisons le modèle Python .gitignore
qui peut être trouvé . Notez que cela est crucial pour s'assurer que notre fichier .env
n'est jamais suivi - si à un moment donné, notre fichier .env
a été suivi, même si nous avons supprimé le fichier dans les étapes ultérieures, le dommage est fait et les acteurs malveillants peuvent retracer le précédent correctifs pour les informations sensibles.
Maintenant que la configuration est terminée, poussons nos modifications vers GitHub. En fonction des métadonnées spécifiées dans pyproject.toml
, n'oubliez pas de mettre à jour votre LICENCE et l'URL de votre page d'accueil en conséquence. Pour référence sur la façon d'écrire de meilleurs commits :
En fonction des exigences fonctionnelles, notre principale préoccupation est de permettre aux utilisateurs d'ajouter une nouvelle carte. Référencer la méthode dans py-trello
: . Pour ce faire, il faut appeler la méthode add_card
de la classe List
, dont on peut récupérer la fonction get_list
de la classe Board
, dont on peut récupérer…
Avant de plonger dans le code, modifions notre fichier pyproject.toml
pour inclure les dépendances nécessaires à l'écriture/exécution des tests unitaires.
# pyproject.toml [project] dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1" ]
Ensuite, activons notre virtualenv et exécutons pip install .
pour installer les dépendances.
# tests/test_trelloservice.py # module imports from trellocli import SUCCESS from trellocli.trelloservice import TrelloService from trellocli.models import * # dependencies imports # misc imports def test_get_access_token(mocker): """Test to check success retrieval of user's access token""" mock_res = GetOAuthTokenResponse( token="test", token_secret="test", status_code=SUCCESS ) mocker.patch( "trellocli.trelloservice.TrelloService.get_user_oauth_token", return_value=mock_res ) trellojob = TrelloService() res = trellojob.get_user_oauth_token() assert res.status_code == SUCCESS
Remarquez dans mon exemple de code que GetOAuthTokenResponse
est un modèle qui n'a pas encore été défini dans models.py
. Il fournit une structure pour écrire du code plus propre, nous verrons cela en action plus tard.
Pour exécuter nos tests, exécutez simplement python -m pytest
. Remarquez comment nos tests échoueront, mais ce n'est pas grave - cela finira par s'arranger.
Challenge Corner 💡 Pouvez-vous essayer d'écrire plus de tests par vous-même ? N'hésitez pas à vous référer à pour voir à quoi ressemblent mes tests
Pour l'instant, construisons notre trelloservice
. En commençant par ajouter une nouvelle dépendance, c'est le wrapper py-trello
.
# pyproject.toml dependencies = [ "pytest==7.4.0", "pytest-mock==3.11.1", "py-trello==0.19.0" ]
Encore une fois, lancez pip install .
pour installer les dépendances.
Maintenant, commençons par construire nos modèles - pour réguler les réponses que nous attendons dans trelloservice
. Pour cette partie, il est préférable de se référer à nos tests unitaires et au code source py-trello
pour comprendre le type de valeur de retour auquel nous pouvons nous attendre.
Par exemple, disons que nous voulons récupérer le jeton d'accès de l'utilisateur, en nous référant à la fonction create_oauth_token
de py-trello
( ), nous savons que la valeur de retour doit ressembler à ceci
# trellocli/models.py # module imports # dependencies imports # misc imports from typing import NamedTuple class GetOAuthTokenResponse(NamedTuple): token: str token_secret: str status_code: int
D'autre part, soyez conscient des conventions de nommage conflictuelles. Par exemple, le module py-trello
a une classe nommée List
. Une solution de contournement consisterait à fournir un alias lors de l'importation.
# trellocli/models.py # dependencies imports from trello import List as Trellolist
# trellocli/models.py class GetBoardName(NamedTuple): """Model to store board id Attributes id (str): Extracted board id from Board value type """ id: str
Challenge Corner 💡 Pouvez-vous essayer d'écrire plus de modèles par vous-même ? N'hésitez pas à vous référer à pour voir à quoi ressemblent mes modèles
Modèles en bas, commençons officiellement à coder le trelloservice
. Encore une fois, nous devrions nous référer aux tests unitaires que nous avons créés - disons que la liste actuelle des tests ne fournit pas une couverture complète pour le service, revenez toujours et ajoutez plus de tests si nécessaire.
Comme d'habitude, incluez toutes les instructions d'importation vers le haut. Créez ensuite la classe TrelloService
et les méthodes d'espace réservé comme prévu. L'idée est que nous allons initialiser une instance partagée du service dans cli.py
et appeler ses méthodes en conséquence. De plus, nous visons l'évolutivité, d'où le besoin d'une couverture étendue.
# trellocli/trelloservice.py # module imports from trellocli import TRELLO_READ_ERROR, TRELLO_WRITE_ERROR, SUCCESS from trellocli.models import * # dependencies imports from trello import TrelloClient # misc imports class TrelloService: """Class to implement the business logic needed to interact with Trello""" def __init__(self) -> None: pass def get_user_oauth_token() -> GetOAuthTokenResponse: pass def get_all_boards() -> GetAllBoardsResponse: pass def get_board() -> GetBoardResponse: pass def get_all_lists() -> GetAllListsResponse: pass def get_list() -> GetListResponse: pass def get_all_labels() -> GetAllLabelsResponse: pass def get_label() -> GetLabelResponse: pass def add_card() -> AddCardResponse: pass
Ps remarquez comment cette fois-ci, lorsque nous exécutons nos tests, nos tests passeront. En fait, cela nous aidera à nous assurer que nous restons sur la bonne voie. Le flux de travail devrait consister à étendre nos fonctions, à exécuter nos tests, à vérifier la réussite/l'échec et à refactoriser en conséquence.
Commençons par la fonction __init__
. L'idée est d'appeler la fonction get_user_oauth_token
ici et d'initialiser le TrelloClient
. Encore une fois, soulignant la nécessité de stocker ces informations sensibles uniquement dans le fichier .env
, nous utiliserons la dépendance python-dotenv
pour récupérer les informations sensibles. Après avoir modifié notre fichier pyproject.toml
en conséquence, commençons à implémenter les étapes d'autorisation.
# trellocli/trelloservice.py class TrelloService: """Class to implement the business logic needed to interact with Trello""" def __init__(self) -> None: self.__load_oauth_token_env_var() self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) def __load_oauth_token_env_var(self) -> None: """Private method to store user's oauth token as an environment variable""" load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): res = self.get_user_oauth_token() if res.status_code == SUCCESS: dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_OAUTH_TOKEN", value_to_set=res.token ) else: print("User denied access.") self.__load_oauth_token_env_var() def get_user_oauth_token(self) -> GetOAuthTokenResponse: """Helper method to retrieve user's oauth token Returns GetOAuthTokenResponse: user's oauth token """ try: res = create_oauth_token() return GetOAuthTokenResponse( token=res["oauth_token"], token_secret=res["oauth_token_secret"], status_code=SUCCESS ) except: return GetOAuthTokenResponse( token="", token_secret="", status_code=TRELLO_AUTHORIZATION_ERROR )
Dans cette implémentation, nous avons créé une méthode d'assistance pour gérer toutes les erreurs prévisibles, par exemple lorsque l'utilisateur clique sur Deny
lors de l'autorisation. De plus, il est configuré pour demander récursivement l'autorisation de l'utilisateur jusqu'à ce qu'une réponse valide soit renvoyée, car le fait est que nous ne pouvons pas continuer tant que l'utilisateur n'autorise pas notre application à accéder aux données de son compte.
Challenge Corner 💡 Avis TRELLO_AUTHORIZATION_ERROR
? Pouvez-vous déclarer cette erreur en tant que constante de package ? Reportez-vous à Configuration pour plus d'informations
# trellocli/trelloservice.py def get_all_boards(self) -> GetAllBoardsResponse: """Method to list all boards from user's account Returns GetAllBoardsResponse: array of user's trello boards """ try: res = self.__client.list_boards() return GetAllBoardsResponse( res=res, status_code=SUCCESS ) except: return GetAllBoardsResponse( res=[], status_code=TRELLO_READ_ERROR ) def get_board(self, board_id: str) -> GetBoardResponse: """Method to retrieve board Required Args board_id (str): board id Returns GetBoardResponse: trello board """ try: res = self.__client.get_board(board_id=board_id) return GetBoardResponse( res=res, status_code=SUCCESS ) except: return GetBoardResponse( res=None, status_code=TRELLO_READ_ERROR )
Comme pour récupérer les listes (colonnes), il va falloir sortir la classe Board
de py-trello
, autrement dit, il faut accepter un nouveau paramètre de type valeur Board
.
# trellocli/trelloservice.py def get_all_lists(self, board: Board) -> GetAllListsResponse: """Method to list all lists (columns) from the trello board Required Args board (Board): trello board Returns GetAllListsResponse: array of trello lists """ try: res = board.all_lists() return GetAllListsResponse( res=res, status_code=SUCCESS ) except: return GetAllListsResponse( res=[], status_code=TRELLO_READ_ERROR ) def get_list(self, board: Board, list_id: str) -> GetListResponse: """Method to retrieve list (column) from the trello board Required Args board (Board): trello board list_id (str): list id Returns GetListResponse: trello list """ try: res = board.get_list(list_id=list_id) return GetListResponse( res=res, status_code=SUCCESS ) except: return GetListResponse( res=None, status_code=TRELLO_READ_ERROR )
Challenge Corner 💡 Pourriez-vous implémenter vous-même les fonctions get_all_labels
et get_label
? Révisez la classe Board
de py-trello
. N'hésitez pas à vous référer à pour voir à quoi ressemble mon implémentation
# trellocli/trelloservice.py def add_card( self, col: Trellolist, name: str, desc: str = "", labels: List[Label] = [] ) -> AddCardResponse: """Method to add a new card to a list (column) on the trello board Required Args col (Trellolist): trello list name (str): card name Optional Args desc (str): card description labels (List[Label]): list of labels to be added to the card Returns AddCardResponse: newly-added card """ try: # create new card new_card = col.add_card(name=name) # add optional description if desc: new_card.set_description(description=desc) # add optional labels if labels: for label in labels: new_card.add_label(label=label) return AddCardResponse( res=new_card, status_code=SUCCESS ) except: return AddCardResponse( res=new_card, status_code=TRELLO_WRITE_ERROR )
Toutes nos félicitations! Tu as gagné. Rejouer (o/N) ?