Skip to content

Commit b6c84ab

Browse files
Feat: Afficheur CLI
- Ajout de la classe AfficheurCLI qui contient les méthodes pour intéragir avec la CLI - Ajout des tests unitaires d'AfficheurCLI - Ajout de la documentation d'AfficheurCLI - Ajout du dossier assets qui contient les éléments visuels de l'application - Ajout de animations.json dans assets qui contient les ASCIIs des animations
1 parent df52607 commit b6c84ab

11 files changed

Lines changed: 490 additions & 28 deletions

File tree

.github/workflows/documentation.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
python -m pip install --upgrade pip
2929
pip install sphinx
3030
pip install sphinx_rtd_theme --break-system-packages
31+
pip install colorama
3132
3233
- name: Construction de la documentation (avec Sphinx)
3334
run: |

.github/workflows/tests.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ jobs:
3939
# Étape 4 : Lancer les tests unitaires
4040
- name: Lancer les tests unitaires
4141
run: |
42-
pytest tests/ --basetemp=resultats_pytest --verbose --cov=app --cov-report=term-missing --cov-report=xml:resultats_pytest/tests-couverture.xml --junitxml=resultats_pytest/tests-rapport.xml
42+
cd tests
43+
pytest --basetemp=resultats_pytest --verbose --cov=../app --cov-report=term-missing --cov-report=xml:resultats_pytest/tests-couverture.xml --junitxml=resultats_pytest/tests-rapport.xml
4344
4445
# Étape 5 : Sauvegarder les artefacts
4546
- name: Sauvegarder les résultats de test
@@ -48,4 +49,4 @@ jobs:
4849
with:
4950
if-no-files-found: error
5051
name: tests-resultats-python-${{ matrix.python-version }} # Nom de l'artefact
51-
path: resultats_pytest # Eléments à sauvegarder
52+
path: tests/resultats_pytest # Eléments à sauvegarder

app/cli/afficheur_cli.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""
2+
Module pour les intéractions avec la ligne de commande.
3+
"""
4+
5+
import sys
6+
from pathlib import Path
7+
from json import load
8+
from time import sleep
9+
from random import choice
10+
import colorama
11+
import threading
12+
13+
class AfficheurCLI:
14+
"""
15+
Représente une classe pour afficher des informations dans la ligne de commande.
16+
17+
Attributes:
18+
_thread_chargement (Union[None,Thread]): Le thread de l'animation de chargement.
19+
_thread_chargement_termine (Event): L'évènement pour demander au thread de
20+
l'animation de chargement de s'arrêter lorsque le chargement est terminé.
21+
_thread_chargement_erreur (Event): L'évènement pour demander au thread de
22+
l'animation de chargement de s'arrêter lorsque une erreur s'est produite.
23+
_animations_actuelles (dict): Les éléments visuels pour l'animation de chargement.
24+
25+
Class-level variables:
26+
:cvar COULEUR_MESSAGE_NORMAL (str): La couleur pour les messages normaux en CLI.
27+
:cvar COULEUR_MESSAGE_ERREUR (str): La couleur pour les messages d'erreur en CLI.
28+
"""
29+
COULEUR_MESSAGE_NORMAL = colorama.Fore.WHITE
30+
COULEUR_MESSAGE_ERREUR = colorama.Fore.RED
31+
32+
def __init__(self):
33+
"""
34+
Initialise un objet pour afficher des informations dans la ligne de commande.
35+
"""
36+
# Normalise les codes couleurs pour fonctionner partout
37+
colorama.init()
38+
# Initialise les variables pour le chargement
39+
self._thread_chargement = None
40+
self._thread_chargement_termine = threading.Event()
41+
self._thread_chargement_erreur = threading.Event()
42+
# Récupère les animations
43+
chemin_racine = Path(__file__).parent.parent.parent.resolve()
44+
chemin_animations = chemin_racine / "assets" / "animations.json"
45+
with open(chemin_animations, "r", encoding="utf-8") as animations:
46+
elements_animations = load(animations)
47+
# Choisis une animation au hasard parmi chaque catégorie d'animation
48+
self._animations_actuelles = {
49+
"chasseur": choice(elements_animations["chasseurs"]),
50+
"fantome": choice(elements_animations["fantomes"]),
51+
"rayon_laser": elements_animations["rayons_laser"]
52+
}
53+
54+
def reecrire_ligne(self, message: str):
55+
"""
56+
Permet d'écrire des caractères par dessus la dernière ligne dans la
57+
ligne de commande.
58+
59+
Args:
60+
message (str): Les caractères à afficher.
61+
62+
Returns:
63+
None
64+
65+
Raises:
66+
TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères.
67+
"""
68+
# Validation du paramètre
69+
if not isinstance(message, str):
70+
raise TypeError("Le message pour la réécriture doit être une chaîne de caractères.")
71+
# Ecriture du message
72+
sys.stdout.write("\r" + self.COULEUR_MESSAGE_NORMAL + message)
73+
sys.stdout.flush()
74+
75+
def affiche_message(self, message: str):
76+
"""
77+
Permet d'écrire un message commun dans la ligne de commande avec la bonne
78+
couleur.
79+
80+
Args:
81+
message (str): Le message à afficher.
82+
83+
Returns:
84+
None
85+
86+
Raises:
87+
TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères.
88+
"""
89+
# Validation du paramètre
90+
if not isinstance(message, str):
91+
raise TypeError("Le message doit être une chaîne de caractères.")
92+
# Ecriture du message
93+
print(self.COULEUR_MESSAGE_NORMAL + message, flush=True)
94+
95+
def affiche_erreur(self, message: str, exception: Exception):
96+
"""
97+
Permet d'écrire un message d'erreur dans la ligne de commande avec la bonne
98+
couleur.
99+
100+
Args:
101+
message (str): Le message à afficher.
102+
exception (Exception): L'exception à afficher.
103+
104+
Returns:
105+
None
106+
107+
Raises:
108+
TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères ou
109+
le paramètre ``exception`` n'est pas une instance de la classe :class:`Exception`.
110+
"""
111+
# Validation des paramètres
112+
if not isinstance(message, str):
113+
raise TypeError("Le message d'erreur doit être une chaîne de caractères.")
114+
if not isinstance(exception, Exception):
115+
raise TypeError("L'exception à afficher doit être une instance de Exception.")
116+
# Ecriture du message
117+
print(self.COULEUR_MESSAGE_ERREUR + f"{message}\n{exception}", flush=True)
118+
119+
def lance_animation_chargement(self):
120+
"""
121+
Lance une animation de chargement dans la ligne de commande via un thread non bloquant.
122+
Si l'animation de chargement est déjà en cours, cette méthode ne fait rien.
123+
124+
Returns:
125+
None
126+
"""
127+
# Si le thread est déjà lancé, annulation de l'animation
128+
if self._thread_chargement is None:
129+
# On réinitialise les demandes d'arrêt
130+
self._thread_chargement_termine.clear()
131+
self._thread_chargement_erreur.clear()
132+
# Initialisation du thread pour le chargement
133+
self._thread_chargement = threading.Thread(target=self._animation_chargement, daemon=True)
134+
# Lancement du thread pour le chargement
135+
self._thread_chargement.start()
136+
137+
def _animation_chargement(self):
138+
"""
139+
Lance l'animation de chargement en boucle jusqu'à la demande d'arrêt via
140+
l'attribut :attr:`_thread_chargement_demande_arret`.
141+
142+
Returns:
143+
None
144+
"""
145+
# Eléments de l'animation de chargement
146+
chasseur_chargement = self._animations_actuelles["chasseur"][0]
147+
fantome_chargement = self._animations_actuelles["fantome"][0]
148+
signes_rayon_laser = self._animations_actuelles["rayon_laser"]
149+
couleurs = ["\033[91m", "\033[93m", "\033[94m", "\033[95m"] # Rouge, Jaune, Bleu, Magenta
150+
151+
# Eléments de l'animation de fin de chargement en cas de succès
152+
chasseur_gagne = self._animations_actuelles["chasseur"][2]
153+
fantome_perd = self._animations_actuelles["fantome"][1]
154+
155+
# Eléments de l'animation de fin de chargement en cas d'erreur
156+
chasseur_perd = self._animations_actuelles["chasseur"][1]
157+
fantome_gagne = self._animations_actuelles["fantome"][2]
158+
159+
# Variables pour l'animation de chargement
160+
index_boucle = 0
161+
rayon_laser = ""
162+
163+
# Début de l'animation (jusqu'à la demande d'arrêt)
164+
while not (self._thread_chargement_termine.is_set()
165+
or self._thread_chargement_erreur.is_set()):
166+
# Arrête d'ajouter des caractères lorsque la chaîne est trop longue
167+
if (index_boucle < 40):
168+
# Récupération de la prochaine couleur
169+
couleur_courante = couleurs[(index_boucle % len(couleurs))]
170+
# Récupération du prochain signe du rayon
171+
signe_courant = signes_rayon_laser[(index_boucle % len(signes_rayon_laser))]
172+
# Ajout du dernier signe avec la nouvelle couleur au rayon
173+
rayon_laser += couleur_courante + signe_courant
174+
# Réactualisation de l'animation de chargement
175+
self.reecrire_ligne(f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}")
176+
index_boucle += 1
177+
sleep(0.05)
178+
179+
# Suppression de la ligne de chargement
180+
self.reecrire_ligne("\033[K")
181+
espace_rayon_laser = " " * index_boucle
182+
if (self._thread_chargement_termine.is_set()):
183+
# Message d'animation terminée
184+
self.reecrire_ligne(f"{chasseur_gagne}{espace_rayon_laser}\033[0m{fantome_perd}\n")
185+
self.affiche_message(f"Analyse terminée! We came, we saw, we logged it.")
186+
else:
187+
# Message d'animation erreur
188+
self.reecrire_ligne(f"{chasseur_perd}{espace_rayon_laser}\033[0m{fantome_gagne}\n")
189+
190+
def stop_animation_chargement(self, erreur: bool = False):
191+
"""
192+
Lance une demande d'arrêt au thread qui gère l'animation de chargement
193+
en cours. Si aucune animation n'est en cours, cette méthode ne fait rien.
194+
195+
Args:
196+
erreur (bool): Indique si la demande d'arrêt est dûe à une erreur ou non.
197+
198+
Returns:
199+
None
200+
"""
201+
# Si le thread de chargement existe et est lancé
202+
if self._thread_chargement and self._thread_chargement.is_alive():
203+
# Lancement de la demade d'arrêt
204+
if not erreur:
205+
self._thread_chargement_termine.set()
206+
else:
207+
self._thread_chargement_erreur.set()
208+
# Attente de l'arrêt depuis le thread principal
209+
self._thread_chargement.join()
210+
self._thread_chargement = None

app/main.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""
22
Point d'entrée de l'application LogBuster !
33
"""
4-
5-
import colorama
4+
from cli.afficheur_cli import AfficheurCLI
65
from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException
76
from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException
87
from analyse.analyseur_log_apache import AnalyseurLogApache
@@ -13,20 +12,10 @@ def main():
1312
"""
1413
Point d'entrée de l'application.
1514
"""
16-
colorama.init()
17-
print(colorama.Style.DIM + r"""
18-
.-. .-') .-') .-') _ ('-. _ .-') ,---.
19-
\ ( OO ) ( OO ). ( OO) ) _( OO)( \( -O ) | |
20-
,--. .-'),-----. ,----. ;-----.\ ,--. ,--. (_)---\_)/ '._(,------.,------. | |
21-
| |.-') ( OO' .-. ' ' .-./-') | .-. | | | | | / _ | |'--...__)| .---'| /`. '| |
22-
| | OO )/ | | | | | |_( O- )| '-' /_) | | | .-') \ :` `. '--. .--'| | | / | || |
23-
| |`-' |\_) | |\| | | | .--, \| .-. `. | |_|( OO ) '..`''.) | | (| '--. | |_.' || .'
24-
(| '---.' \ | | | |(| | '. (_/| | \ | | | | `-' /.-._) \ | | | .--' | . '.'`--'
25-
| | `' '-' ' | '--' | | '--' /(' '-'(_.-' \ / | | | `---.| |\ \ .--.
26-
`------' `-----' `------' `------' `-----' `-----' `--' `------'`--' '--''--'
27-
28-
""")
15+
afficheur_cli = AfficheurCLI()
16+
afficheur_cli.affiche_message("Who ya gonna call? LogBuster!")
2917
try:
18+
afficheur_cli.lance_animation_chargement()
3019
# Récupération des arguments
3120
parseur_cli = ParseurArgumentsCLI()
3221
arguments_cli = parseur_cli.parse_args()
@@ -39,17 +28,33 @@ def main():
3928
# Exportation de l'analyse
4029
exporteur = Exporteur(arguments_cli.sortie)
4130
exporteur.export_vers_json(analyse)
42-
except ArgumentCLIException as ex:
43-
print(f"Erreur dans les arguments fournis !\n {ex}")
44-
except FileNotFoundError as ex:
45-
print(f"Erreur dans la recherche du log Apache !\n{ex}")
46-
except FormatLogApacheInvalideException as ex:
47-
print(f"Erreur dans l'analyse du log Apache !\n{ex}")
48-
except ExportationException as ex:
49-
print(f"Erreur dans l'exportation de l'analyse !\n{ex}")
31+
afficheur_cli.stop_animation_chargement()
5032
except Exception as ex:
51-
print(f"Erreur interne !\n{ex}")
33+
gestion_exception(afficheur_cli, ex)
34+
35+
def gestion_exception(afficheur_cli, exception):
36+
"""
37+
Gère les erreurs qui demandent une fin du programme.
38+
Affiche également un message d'erreur personnalisé en fonction
39+
de l'exception.
5240
41+
Args:
42+
afficheur_cli (AfficheurCLI): L'objet permettant d'intéragir avec la ligne
43+
de commande.
44+
exception (Exception): L'exception qui s'est produite.
45+
46+
Returns:
47+
None
48+
"""
49+
erreurs = {
50+
ArgumentCLIException: "Erreur dans les arguments fournis !",
51+
FileNotFoundError: "Erreur dans la recherche du log Apache !",
52+
FormatLogApacheInvalideException: "Erreur dans l'analyse du log Apache !",
53+
ExportationException: "Erreur dans l'exportation de l'analyse !"
54+
}
55+
message = erreurs.get(type(exception), "Erreur interne !")
56+
afficheur_cli.stop_animation_chargement(True)
57+
afficheur_cli.affiche_erreur(message, exception)
5358

5459
if __name__ == "__main__":
5560
main()

assets/animations.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"chasseurs": [
3+
["(҂-_•)⊃═O", "(҂x_x)", "d(•᎑-҂)"],
4+
["(⌐■_■)⊃═O", "(⌐x_x)", "d(■᎑■⌐)"],
5+
["(∩•_•)⊃═O", "(∩x_x)", "d(•᎑•∩)"],
6+
["(ò_ó)⊃═O", "(x_x)", "d(ò᎑ó)"]
7+
],
8+
9+
"fantomes": [
10+
["ε=(( ꐑº-° )ꐑ", "(( x-x)", "ε=(( ꐑº᎑° )ꐑ"],
11+
["ε=(¬ ´ཀ` )¬", "( xཀx)", "ε=(¬ ´᎑` )¬"],
12+
["ε=༼ つ ╹ ╹ ༽つ", "༼ x x ༽", "ε=༼ つ ╹᎑╹ ༽つ"],
13+
["ε=( >_<)", "( x_x)", "ε=( >᎑<)"],
14+
["ε=(ง'̀-'́)ง", "('x-x)", "ε=(ง'̀᎑'́)ง"]
15+
],
16+
17+
"rayons_laser": ["-", "=", "~", "*", "~", "=", "-"]
18+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
AfficheurCLI
2+
======================
3+
4+
.. automodule:: cli.afficheur_cli
5+
:members:
6+
:show-inheritance:
7+
:undoc-members:

docs/source/modules/cli/index_cli.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ CLI
55
:maxdepth: 4
66

77
parseur_arguments_cli.rst
8+
afficheur_cli.rst

tests/.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[report]
2+
exclude_lines =
3+
if __name__ == "__main__":

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import pytest
6+
from cli.afficheur_cli import AfficheurCLI
67
from cli.parseur_arguments_cli import ParseurArgumentsCLI
78
from parse.parseur_log_apache import ParseurLogApache
89
from analyse.analyseur_log_apache import AnalyseurLogApache
@@ -47,6 +48,10 @@
4748
# Fixtures générales
4849
# ------------------
4950

51+
@pytest.fixture
52+
def afficheur_cli():
53+
return AfficheurCLI()
54+
5055
@pytest.fixture
5156
def parseur_arguments_cli():
5257
"""

0 commit comments

Comments
 (0)