Skip to content

Commit 3cf8e60

Browse files
Feat: Intéraction CLI (#31)
- 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 3cf8e60

11 files changed

Lines changed: 567 additions & 27 deletions

File tree

.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

analyse-log-apache.json

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"chemin": "C:\\Users\\Work\\App\\Xampp\\apache\\logs\\access.log",
3+
"statistiques": {
4+
"total_entrees": 17594,
5+
"requetes": {
6+
"top_urls": [
7+
{
8+
"url": "/assets/icons/muted.png",
9+
"total": 665,
10+
"taux": 3.7796976241900646
11+
},
12+
{
13+
"url": "/style/Header.css",
14+
"total": 616,
15+
"taux": 3.501193588723429
16+
},
17+
{
18+
"url": "/assets/icon.ico",
19+
"total": 595,
20+
"taux": 3.381834716380584
21+
}
22+
],
23+
"repartition_code_statut_http": [
24+
{
25+
"code": 200,
26+
"total": 14996,
27+
"taux": 85.23360236444243
28+
},
29+
{
30+
"code": 304,
31+
"total": 1494,
32+
"taux": 8.491531203819484
33+
},
34+
{
35+
"code": 206,
36+
"total": 604,
37+
"taux": 3.432988518813232
38+
},
39+
{
40+
"code": 404,
41+
"total": 230,
42+
"taux": 1.307263839945436
43+
},
44+
{
45+
"code": 302,
46+
"total": 188,
47+
"taux": 1.0685460952597476
48+
},
49+
{
50+
"code": 403,
51+
"total": 40,
52+
"taux": 0.22735023303398885
53+
},
54+
{
55+
"code": 500,
56+
"total": 20,
57+
"taux": 0.11367511651699443
58+
},
59+
{
60+
"code": 301,
61+
"total": 13,
62+
"taux": 0.07388882573604638
63+
},
64+
{
65+
"code": 400,
66+
"total": 6,
67+
"taux": 0.03410253495509833
68+
},
69+
{
70+
"code": 408,
71+
"total": 3,
72+
"taux": 0.017051267477549165
73+
}
74+
]
75+
}
76+
}
77+
}

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: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""
22
Point d'entrée de l'application LogBuster !
33
"""
4-
4+
from time import sleep
55
import colorama
6+
from cli.afficheur_cli import AfficheurCLI
67
from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException
78
from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException
89
from analyse.analyseur_log_apache import AnalyseurLogApache
@@ -13,20 +14,10 @@ def main():
1314
"""
1415
Point d'entrée de l'application.
1516
"""
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-
""")
17+
afficheur_cli = AfficheurCLI()
18+
afficheur_cli.affiche_message("Who ya gonna call? LogBuster!")
2919
try:
20+
afficheur_cli.lance_animation_chargement()
3021
# Récupération des arguments
3122
parseur_cli = ParseurArgumentsCLI()
3223
arguments_cli = parseur_cli.parse_args()
@@ -39,17 +30,33 @@ def main():
3930
# Exportation de l'analyse
4031
exporteur = Exporteur(arguments_cli.sortie)
4132
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}")
33+
afficheur_cli.stop_animation_chargement()
5034
except Exception as ex:
51-
print(f"Erreur interne !\n{ex}")
35+
gestion_exception(afficheur_cli, ex)
36+
37+
def gestion_exception(afficheur_cli, exception):
38+
"""
39+
Gère les erreurs qui demandent une fin du programme.
40+
Affiche également un message d'erreur personnalisé en fonction
41+
de l'exception.
5242
43+
Args:
44+
afficheur_cli (AfficheurCLI): L'objet permettant d'intéragir avec la ligne
45+
de commande.
46+
exception (Exception): L'exception qui s'est produite.
47+
48+
Returns:
49+
None
50+
"""
51+
erreurs = {
52+
ArgumentCLIException: "Erreur dans les arguments fournis !",
53+
FileNotFoundError: "Erreur dans la recherche du log Apache !",
54+
FormatLogApacheInvalideException: "Erreur dans l'analyse du log Apache !",
55+
ExportationException: "Erreur dans l'exportation de l'analyse !"
56+
}
57+
message = erreurs.get(type(exception), "Erreur interne !")
58+
afficheur_cli.stop_animation_chargement(True)
59+
afficheur_cli.affiche_erreur(message, exception)
5360

5461
if __name__ == "__main__":
5562
main()

0 commit comments

Comments
 (0)