Skip to content

Commit a77cecd

Browse files
Refractor: Mise en conformité avec PEP8 (#34)
- Mise en conformité du code avec PEP8 - Déplacement de la classe EntreeLogApache vers le fichier entree_log_apache.py - Mise à jour de la documentation suite à ces modifications - Mise à jour des tests unitaires suite à ces modifications - Ajout d'une action pour tester si la qualité du code est noté correctement par PyLint (>= 9)
1 parent a2f805a commit a77cecd

27 files changed

Lines changed: 761 additions & 168 deletions

.github/workflows/documentation.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Construction et déploiement de la configuration
1+
name: Documentation - LogBuster
22

33
on:
44
push:

.github/workflows/qualite.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Qualité code - LogBuster
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
8+
jobs:
9+
lint:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout du code
14+
uses: actions/checkout@v4
15+
16+
- name: Configuration de Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.x'
20+
21+
- name: Installation des dépendances
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install pylint
25+
pip install colorama
26+
27+
- name: Analyse avec Pylint (note >= 9.0 requise)
28+
run: |
29+
pylint app > tests-resultats-qualite.txt || true
30+
SCORE=$(grep "Your code has been rated at" tests-resultats-qualite.txt | awk '{print $7}' | cut -d"/" -f1)
31+
echo "Le score du code dans le dossier app est de $SCORE"
32+
SCORE_VALIDE=$(echo "$SCORE >= 9.0" | bc)
33+
if [ "$SCORE_VALIDE" -ne 1 ]; then
34+
echo "Erreur: La note du code est inférieur à 9."
35+
exit 1
36+
fi
37+
38+
- name: Upload du rapport Pylint
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: rapport-qualite-code
42+
path: tests-resultats-qualite.txt

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ jobs:
4848
uses: actions/upload-artifact@v4
4949
with:
5050
if-no-files-found: error
51-
name: tests-resultats-python-${{ matrix.python-version }} # Nom de l'artefact
51+
name: rapport-tests-unitaires-python-${{ matrix.python-version }} # Nom de l'artefact
5252
path: tests/resultats_pytest # Eléments à sauvegarder

app/analyse/analyseur_log_apache.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,27 @@ def __init__(self, fichier_log_apache: FichierLogApache, nombre_par_top: int = 3
2727
les statistiques des classements (tops). Par défaut, sa valeur est égale à ``3``.
2828
2929
Raises:
30-
TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance de :class:`FichierLogApache`
30+
TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance
31+
de :class:`FichierLogApache`
3132
ou si l'argument ``nombre_par_top`` n'est pas un entier.
3233
ValueError: Si l'argument ``nombre_par_top`` est inférieur à ``0``.
3334
"""
35+
# Vérification du type des paramètres
3436
if not isinstance(fichier_log_apache, FichierLogApache):
3537
raise TypeError("La représentation du fichier doit être de type FichierLogApache.")
3638
if not isinstance(nombre_par_top, int) or isinstance(nombre_par_top, bool):
3739
raise TypeError("Le nombre par top doit être un entier.")
40+
# Vérification de la valeur du paramètre
3841
if nombre_par_top < 0:
3942
raise ValueError("Le nombre par top doit être supérieur ou égale à 0.")
43+
44+
# Ajout des données
4045
self.fichier = fichier_log_apache
4146
self.nombre_par_top = nombre_par_top
4247

4348
def _get_repartition_elements(self,
44-
liste_elements: list,
45-
nom_elements: str,
49+
liste_elements: list,
50+
nom_elements: str,
4651
mode_top_classement: bool = False) -> list:
4752
"""
4853
Retourne le top 'n' des éléments qui apparaissent le plus dans la liste.
@@ -64,21 +69,25 @@ def _get_repartition_elements(self,
6469
:attr:`nombre_par_top` éléments, mais peut en contenir moins s'il y a moins
6570
de :attr:`nombre_par_top` éléments distincts.
6671
"""
72+
# Vérification du type des paramètres
6773
if not isinstance(liste_elements, list):
6874
raise TypeError("La liste des éléments doit être une instance du type list.")
6975
if not isinstance(nom_elements, str):
7076
raise TypeError("Le nom des éléments doit être une chaîne de caractères")
7177
if not isinstance(mode_top_classement, bool):
72-
raise TypeError("L'indication de l'activation du mode 'top_classement' doi être un booléen.")
73-
78+
raise TypeError("L'indication de l'activation du mode 'top_classement' "
79+
"doit être un booléen.")
80+
81+
# Analyse de la liste
7482
total_elements = len(liste_elements)
7583
compteur_elements = Counter(liste_elements)
76-
top_elements = compteur_elements.most_common(self.nombre_par_top if mode_top_classement else None)
84+
top_elements = compteur_elements.most_common(self.nombre_par_top
85+
if mode_top_classement else None)
7786
return [
7887
{nom_elements: element, "total": total, "taux": total / total_elements * 100}
7988
for element, total in top_elements
8089
]
81-
90+
8291
def get_analyse_complete(self) -> dict:
8392
"""
8493
Retourne l'analyse complète du fichier de log Apache.
@@ -88,7 +97,8 @@ def get_analyse_complete(self) -> dict:
8897
- statistiques:
8998
- requetes:
9099
- top_urls: voir :meth:`get_top_urls`
91-
- repartition_code_statut_http: voir :meth:`get_total_par_code_statut_http`
100+
- repartition_code_statut_http:
101+
voir :meth:`get_total_par_code_statut_http`
92102
93103
Returns:
94104
dict: L'analyse sous forme d'un dictionnaire.
@@ -112,7 +122,7 @@ def get_total_entrees(self) -> int:
112122
int: Le nombre total d'entrées.
113123
"""
114124
return len(self.fichier.entrees)
115-
125+
116126
def get_top_urls(self) -> list:
117127
"""
118128
Retourne le top :attr:`nombre_par_top` des urls les plus demandées.
@@ -130,7 +140,7 @@ def get_top_urls(self) -> list:
130140
"url",
131141
True
132142
)
133-
143+
134144
def get_total_par_code_statut_http(self) -> list:
135145
"""
136146
Retourne la répartition des réponses par code de statut htpp retourné.
@@ -146,4 +156,4 @@ def get_total_par_code_statut_http(self) -> list:
146156
return self._get_repartition_elements(
147157
[entree.reponse.code_statut_http for entree in self.fichier.entrees],
148158
"code"
149-
)
159+
)

app/cli/afficheur_cli.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from json import load
88
from time import sleep
99
from random import choice
10-
import colorama
1110
import threading
11+
import colorama
12+
1213

1314
class AfficheurCLI:
1415
"""
@@ -51,7 +52,7 @@ def __init__(self):
5152
"rayon_laser": elements_animations["rayons_laser"]
5253
}
5354

54-
def reecrire_ligne(self, message: str):
55+
def reecrire_ligne(self, message: str) -> None:
5556
"""
5657
Permet d'écrire des caractères par dessus la dernière ligne dans la
5758
ligne de commande.
@@ -68,11 +69,12 @@ def reecrire_ligne(self, message: str):
6869
# Validation du paramètre
6970
if not isinstance(message, str):
7071
raise TypeError("Le message pour la réécriture doit être une chaîne de caractères.")
72+
7173
# Ecriture du message
7274
sys.stdout.write("\r" + self.COULEUR_MESSAGE_NORMAL + message)
7375
sys.stdout.flush()
7476

75-
def affiche_message(self, message: str):
77+
def affiche_message(self, message: str) -> None:
7678
"""
7779
Permet d'écrire un message commun dans la ligne de commande avec la bonne
7880
couleur.
@@ -89,10 +91,11 @@ def affiche_message(self, message: str):
8991
# Validation du paramètre
9092
if not isinstance(message, str):
9193
raise TypeError("Le message doit être une chaîne de caractères.")
94+
9295
# Ecriture du message
9396
print(self.COULEUR_MESSAGE_NORMAL + message, flush=True)
9497

95-
def affiche_erreur(self, message: str, exception: Exception):
98+
def affiche_erreur(self, message: str, exception: Exception) -> None:
9699
"""
97100
Permet d'écrire un message d'erreur dans la ligne de commande avec la bonne
98101
couleur.
@@ -113,10 +116,11 @@ def affiche_erreur(self, message: str, exception: Exception):
113116
raise TypeError("Le message d'erreur doit être une chaîne de caractères.")
114117
if not isinstance(exception, Exception):
115118
raise TypeError("L'exception à afficher doit être une instance de Exception.")
119+
116120
# Ecriture du message
117121
print(self.COULEUR_MESSAGE_ERREUR + f"{message}\n{exception}", flush=True)
118122

119-
def lance_animation_chargement(self):
123+
def lance_animation_chargement(self) -> None:
120124
"""
121125
Lance une animation de chargement dans la ligne de commande via un thread non bloquant.
122126
Si l'animation de chargement est déjà en cours, cette méthode ne fait rien.
@@ -130,11 +134,12 @@ def lance_animation_chargement(self):
130134
self._thread_chargement_termine.clear()
131135
self._thread_chargement_erreur.clear()
132136
# Initialisation du thread pour le chargement
133-
self._thread_chargement = threading.Thread(target=self._animation_chargement, daemon=True)
137+
self._thread_chargement = threading.Thread(target=self._animation_chargement,
138+
daemon=True)
134139
# Lancement du thread pour le chargement
135140
self._thread_chargement.start()
136-
137-
def _animation_chargement(self):
141+
142+
def _animation_chargement(self) -> None:
138143
"""
139144
Lance l'animation de chargement en boucle jusqu'à la demande d'arrêt via
140145
l'attribut :attr:`_thread_chargement_demande_arret`.
@@ -147,7 +152,7 @@ def _animation_chargement(self):
147152
fantome_chargement = self._animations_actuelles["fantome"][0]
148153
signes_rayon_laser = self._animations_actuelles["rayon_laser"]
149154
couleurs = ["\033[91m", "\033[93m", "\033[94m", "\033[95m"] # Rouge, Jaune, Bleu, Magenta
150-
155+
151156
# Eléments de l'animation de fin de chargement en cas de succès
152157
chasseur_gagne = self._animations_actuelles["chasseur"][2]
153158
fantome_perd = self._animations_actuelles["fantome"][1]
@@ -164,30 +169,32 @@ def _animation_chargement(self):
164169
while not (self._thread_chargement_termine.is_set()
165170
or self._thread_chargement_erreur.is_set()):
166171
# Arrête d'ajouter des caractères lorsque la chaîne est trop longue
167-
if (index_boucle < 40):
172+
if index_boucle < 40:
168173
# Récupération de la prochaine couleur
169174
couleur_courante = couleurs[(index_boucle % len(couleurs))]
170175
# Récupération du prochain signe du rayon
171176
signe_courant = signes_rayon_laser[(index_boucle % len(signes_rayon_laser))]
172177
# Ajout du dernier signe avec la nouvelle couleur au rayon
173178
rayon_laser += couleur_courante + signe_courant
174179
# Réactualisation de l'animation de chargement
175-
self.reecrire_ligne(f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}")
180+
self.reecrire_ligne(
181+
f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}"
182+
)
176183
index_boucle += 1
177184
sleep(0.05)
178185

179186
# Suppression de la ligne de chargement
180187
self.reecrire_ligne("\033[K")
181188
espace_rayon_laser = " " * index_boucle
182-
if (self._thread_chargement_termine.is_set()):
189+
if self._thread_chargement_termine.is_set():
183190
# Message d'animation terminée
184191
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.")
192+
self.affiche_message("Analyse terminée! We came, we saw, we logged it.")
186193
else:
187194
# Message d'animation erreur
188195
self.reecrire_ligne(f"{chasseur_perd}{espace_rayon_laser}\033[0m{fantome_gagne}\n")
189196

190-
def stop_animation_chargement(self, erreur: bool = False):
197+
def stop_animation_chargement(self, erreur: bool = False) -> None:
191198
"""
192199
Lance une demande d'arrêt au thread qui gère l'animation de chargement
193200
en cours. Si aucune animation n'est en cours, cette méthode ne fait rien.
@@ -198,6 +205,10 @@ def stop_animation_chargement(self, erreur: bool = False):
198205
Returns:
199206
None
200207
"""
208+
# Vérification du type du paramètre
209+
if not isinstance(erreur, bool):
210+
raise TypeError("L'indication d'une erreur doit être un booléan.")
211+
201212
# Si le thread de chargement existe et est lancé
202213
if self._thread_chargement and self._thread_chargement.is_alive():
203214
# Lancement de la demade d'arrêt

app/cli/parseur_arguments_cli.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
Module pour analyser les arguments passés en ligne de commande.
33
"""
44

5-
from argparse import ArgumentParser
5+
from argparse import ArgumentParser, Namespace
66
from re import match
7+
from typing import Optional
8+
79

810
class ParseurArgumentsCLI(ArgumentParser):
911
"""
@@ -12,14 +14,20 @@ class ParseurArgumentsCLI(ArgumentParser):
1214
"""
1315

1416
def __init__(self):
17+
"""
18+
Initialise uparseur pour analyser les arguments passés en ligne de commande.
19+
"""
1520
super().__init__(
1621
description="LogBuster, l'analyseur de log Apache.", allow_abbrev=False,
1722
)
1823
self.__set_arguments()
1924

20-
def __set_arguments(self):
25+
def __set_arguments(self) -> None:
2126
"""
2227
Définit les arguments attendus par l'application.
28+
29+
Returns:
30+
None
2331
"""
2432
# -- Argument obligatoire --
2533
self.add_argument(
@@ -37,10 +45,32 @@ def __set_arguments(self):
3745
"nom 'analyse-log-apache.json' dans le repertoire courant sera crée.",
3846
)
3947

40-
def parse_args(self, args=None, namespace=None):
48+
def parse_args(self,
49+
args: Optional[list] = None,
50+
namespace: Optional[Namespace] = None) -> Namespace:
4151
"""
42-
Analyse, vérifie et retourne les arguments fournis en ligne de commande.
52+
Récupère les arguments passés en ligne de commande puis vérifie
53+
que leur format est conforme à ceux attendus.
54+
55+
Args:
56+
args (Optional[list]): Liste des arguments passés en paramètre.
57+
Si ``None``, les arguments de la ligne de commande sont utilisés.
58+
namespace (Optional[Namespace]): Un espace de noms (namespace)
59+
pour stocker les résultats. Si ``None``, un nouvel espace de noms est créé.
60+
61+
Returns:
62+
Namespace: L'objet contenant les arguments analysés et leurs valeurs.
63+
64+
Raises:
65+
ArgumentCLIException: Si une erreur se produit lors du parsing des arguments
66+
(par exemple, si un argument inconnu est fourni ou si son format est invalide).
4367
"""
68+
# Vérification du type des paramètres
69+
if args is not None and not isinstance(args, list):
70+
raise TypeError("Les arguments doivent soit être None, soit être dans une liste.")
71+
if namespace is not None and not isinstance(args, Namespace):
72+
raise TypeError("L'espace de noms doit soit être None, soit être un objet Namespace.")
73+
4474
# Analyse des arguments
4575
try:
4676
arguments_parses = super().parse_args(args, namespace)
@@ -49,29 +79,33 @@ def parse_args(self, args=None, namespace=None):
4979

5080
# Vérification syntaxique des arguments
5181
regex_chemin = r"^[a-zA-Z0-9:_\\\-.\/]+$"
82+
5283
if not match(regex_chemin, arguments_parses.chemin_log):
5384
raise ArgumentCLIException(
5485
"Le chemin du fichier log doit uniquement contenir les caractères autorisés. "
5586
"Les caractères autorisés sont les minuscules, majuscules, chiffres ou les "
5687
"caractères spéciaux suivants: _, \\, -, /."
5788
)
89+
5890
if not match(regex_chemin, arguments_parses.sortie):
5991
raise ArgumentCLIException(
6092
"Le chemin du fichier de sortie doit uniquement contenir les caractères "
6193
"autorisés. Les caractères autorisés sont les minuscules, majuscules, "
6294
"chiffres ou les caractères spéciaux suivants: _, \\, -, /."
6395
)
96+
6497
if not arguments_parses.sortie.endswith(".json"):
6598
raise ArgumentCLIException(
6699
"Le fichier de sortie doit obligatoirement être un fichier au format json."
67100
)
68-
101+
69102
return arguments_parses
70103

71104

72105
class ArgumentCLIException(Exception):
73106
"""
74-
Représente une erreur liée l'analyse d'un argument en ligne de commande.
107+
Représente une erreur lorsque un argument passé en ligne de commande
108+
est inconnu ou que son format est invalide.
75109
"""
76110

77111
def __init__(self, *args):

0 commit comments

Comments
 (0)