Skip to content

Commit 45ea106

Browse files
Feat: Ajout de l'exportation de graphique camembert (#46)
- Ajout d'un nouvel argument --camembert - Ajout d'une méthode pour exporter un camember dans Exporteur - Ajout d'une méthode pour retourner les code de statut dans un format pour les camemberts dans AnalyseurLogApache - Mise à jour des tests unitaires pour suivre ces changements
1 parent e15728a commit 45ea106

11 files changed

Lines changed: 342 additions & 143 deletions

.github/workflows/documentation.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ jobs:
3333
pip install sphinx
3434
pip install sphinx_rtd_theme --break-system-packages
3535
pip install colorama
36+
pip install altair
37+
pip install pandas
3638
3739
# Étape 4 : Générer la documentation
3840
- name: Construction de la documentation (avec Sphinx)

.github/workflows/qualite.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
python -m pip install --upgrade pip
2828
pip install pylint
2929
pip install colorama
30+
pip install altair
31+
pip install pandas
3032
3133
# Étape 4 : Lancement de l'analyse
3234
- name: Analyse avec Pylint (note >= 9.0 requise)

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ jobs:
3333
run: |
3434
python -m pip install --upgrade pip
3535
pip install colorama
36+
pip install altair
37+
pip install pandas
3638
pip install pytest
3739
pip install pytest-cov
3840
pip install pytest-mock

app/analyse/analyseur_log_apache.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def get_top_urls(self) -> list:
177177

178178
def get_total_par_code_statut_http(self) -> list:
179179
"""
180-
Retourne la répartition des réponses par code de statut htpp retourné.
180+
Retourne la répartition des réponses par code de statut http retourné.
181181
Les entrées prisent en compte sont uniquement celles qui ont passées le filtre.
182182
183183
Returns:
@@ -192,3 +192,18 @@ def get_total_par_code_statut_http(self) -> list:
192192
[entree.reponse.code_statut_http for entree in self._get_entrees_passent_filtre()],
193193
"code"
194194
)
195+
196+
def get_total_par_code_statut_http_camembert(self) -> list:
197+
"""
198+
Retourne la répartition des réponses par code de statut http retourné sous
199+
un format utilisable par un camembert.
200+
Les entrées prisent en compte sont uniquement celles qui ont passées le filtre.
201+
202+
Returns:
203+
list: Une liste de liste de deux éléments où l'index 0 est le code et l'index 1
204+
son total d'apparition.
205+
"""
206+
return [
207+
[stat["code"], stat["total"]]
208+
for stat in self.get_total_par_code_statut_http()
209+
]

app/cli/parseur_arguments_cli.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ def __set_arguments(self) -> None:
4040
"-s",
4141
"--sortie",
4242
type=str,
43-
default="./analyse-log-apache.json",
44-
help="Fichier JSON où sera écrit l'analyse. Par défaut, un fichier avec le "
45-
"nom 'analyse-log-apache.json' dans le repertoire courant sera crée.",
43+
default="./",
44+
help="Dossier où sera écrit l'analyse du fichier de log Apache. Par défaut,"
45+
"sa valeur est le répertoire d'exécution du script.",
4646
)
4747
self.add_argument(
4848
"-i",
@@ -56,6 +56,11 @@ def __set_arguments(self) -> None:
5656
type=int,
5757
help="Le code de statut http que doivent avoir les entrées à analyser."
5858
)
59+
self.add_argument(
60+
"--camembert",
61+
action="store_true",
62+
help="Active la génération d'histogrammes pour les statistiques compatibles."
63+
)
5964

6065
def parse_args(self,
6166
args: Optional[list] = None,
@@ -106,11 +111,6 @@ def parse_args(self,
106111
"chiffres ou les caractères spéciaux suivants: _, \\, -, /."
107112
)
108113

109-
if not arguments_parses.sortie.endswith(".json"):
110-
raise ArgumentCLIException(
111-
"Le fichier de sortie doit obligatoirement être un fichier au format json."
112-
)
113-
114114
return arguments_parses
115115

116116

app/export/exporteur.py

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
Module pour l'exportation des données.
33
"""
44

5-
from os.path import abspath, dirname, isdir
5+
from os.path import abspath, isdir, join
66
from json import dump
7+
from altair import Chart
8+
from pandas import DataFrame
79

810

911
class Exporteur:
@@ -12,88 +14,131 @@ class Exporteur:
1214
vers un fichier de sortie.
1315
1416
Attributes:
15-
_chemin_sortie (str): Le chemin du fichier vers lequel
16-
les données seront exportées.
17+
_chemin_sortie (str): Le chemin du dossier vers lequel les données
18+
vont être exportées.
1719
"""
1820

1921
def __init__(self, chemin_sortie: str):
20-
"""
21-
Initialise un exporteur de données.
22-
23-
Args:
24-
chemin_sortie (str): Le chemin du fichier vers lequel
25-
les données seront exportées.
26-
27-
Raises:
28-
TypeError: Le chemin de sortie n'est pas une chaîne de caractère.
29-
ExportationDossierParentException: Exportation impossible à cause de
30-
l'inexistance du dossier parent du fichier d'exportation.
31-
"""
32-
# Vérification du type du paramètre
22+
# Vérification du paramètre
3323
if not isinstance(chemin_sortie, str):
34-
raise TypeError("Le chemin de sortie doit être une chaîne de caractère.")
35-
# Vérification du chemin d'exportation
36-
self.verification_exportation_possible(chemin_sortie)
37-
# Ajout du chemin d'exportation
24+
raise TypeError("Le chemin de sortie doit être une chaîne de caractères.")
25+
# Vérification du chemin
26+
chemin_sortie_absolue = abspath(chemin_sortie)
27+
if not isdir(chemin_sortie_absolue):
28+
raise ExportationDossierIntrouvableException(f"Impossible d'exporter vers le "
29+
f"dossier {chemin_sortie} ({chemin_sortie_absolue}), "
30+
"le dossier n'existe pas.")
31+
# Ajout du chemin
3832
self._chemin_sortie = chemin_sortie
3933

40-
def verification_exportation_possible(self, chemin_sortie: str) -> None:
34+
def export_vers_json(self, donnees: dict, nom_fichier: str) -> None:
4135
"""
42-
Vérifie qu'une exportation est possible vers le chemin du fichier indiqué. Renvoie une
43-
exception expliquant le problème si elle n'est pas possible.
36+
Export le dictionnaire fourni vers le ``chemin de sortie``.
4437
4538
Args:
46-
chemin_sortie (str): Le chemin du fichier d'exportation.
39+
donnees (dict): Le dictionnaire qui contient les données.
40+
nom_fichier (str): Le nom du fichier JSON.
4741
4842
Returns:
4943
None
5044
5145
Raises:
52-
ExportationDossierParentException: Le dossier parent du fichier n'existe pas.
46+
TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire.
47+
ExportationJsonException: Une erreur lors de l'écriture dans le fichier JSON.
5348
"""
54-
# Vérification du type du paramètre
55-
if not isinstance(chemin_sortie, str):
56-
raise TypeError("Le chemin de sortie doit être une chaîne de caractères.")
57-
# Vérification du chemin
58-
chemin_sortie_absolue = abspath(chemin_sortie)
59-
dossier_parent = dirname(chemin_sortie_absolue)
60-
if not isdir(dossier_parent):
61-
raise ExportationDossierParentException(f"Impossible d'exporter vers le "
62-
f"fichier {chemin_sortie}, son dossier parent "
63-
f"{dossier_parent} n'existe pas.")
49+
# Vérification du type des paramètres
50+
if not isinstance(donnees, dict):
51+
raise TypeError("Les statistiques à exporter doivent être sous une forme "
52+
"de dictionnaire.")
53+
if not isinstance(nom_fichier, str):
54+
raise TypeError("Le nom du fichier doit être une chaîne de caractère.")
55+
# Vérification du nom du fichier
56+
if not nom_fichier.endswith(".json"):
57+
raise ValueError("Le fichier JSON doit terminé par l'extention '.json'.")
58+
# Exportation
59+
chemin_fichier = join(self._chemin_sortie, nom_fichier)
60+
try:
61+
with open(chemin_fichier, 'w', encoding="utf-8") as fichier:
62+
dump(donnees, fichier, indent=4)
63+
except Exception as ex:
64+
raise ExportationJsonException(str(ex)) from ex
6465

65-
def export_vers_json(self, donnees: dict) -> None:
66+
def export_vers_html_camembert(self,
67+
donnees: list,
68+
nom_fichier: str) -> None:
6669
"""
67-
Export le dictionnaire fourni vers le :attr:`chemin de sortie`.
70+
Export la liste fournie vers un camembert HTML vers le ``chemin de sortie``.
6871
6972
Args:
70-
donnees (dict): Le dictionnaire qui contient les données.
73+
donnees (list): Les données du camembert. La liste doit contenir
74+
des listes de deux éléments où le premier reprèsente le nom de cette
75+
partie du camembert et le deuxième sa valeur.
76+
nom_fichier (str): Le nom du fichier HTML.
7177
7278
Returns:
7379
None
7480
7581
Raises:
76-
TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire.
77-
ExportationException: Une erreur lors de l'écriture dans le fichier JSON.
82+
TypeError: Les paramètres ne sont pas du type attendu ou la liste ``donnees``
83+
contient un élément qui n'est pas une liste.
84+
ValueError: Le paramètre ``nom_fichier`` ne termine pas par .html ou le paramètre
85+
``donnees`` ne contient pas des listes de longueur 2.
86+
ExportationCamembertHtmlException: Erreur lors de l'exportation du camembert.
7887
"""
79-
# Vérification du type du paramètre
80-
if not isinstance(donnees, dict):
81-
raise TypeError("Les données à exporter doivent être sous une forme "
82-
"de dictionnaire.")
88+
# Vérification du type des paramètres
89+
if not isinstance(donnees, list):
90+
raise TypeError("Les données de l'histogramme à exporter doit être sous une forme "
91+
"de liste.")
92+
if not isinstance(nom_fichier, str):
93+
raise TypeError("Le nom du fichier doit être une chaîne de caractère.")
94+
# Vérification du nom du fichier
95+
if not nom_fichier.endswith(".html"):
96+
raise ValueError("Le fichier HTML doit terminé par l'extention '.html'.")
97+
# Récupération des axes du graphique
98+
axe_x = []
99+
axe_y = []
100+
for donnee in donnees:
101+
if not isinstance(donnee, list):
102+
raise ValueError("La liste des données de l'histogramme à exporter ne doit "
103+
"contenir que des listes.")
104+
if not len(donnee) == 2:
105+
raise ValueError("La liste des données de l'histogramme à exporter ne doit "
106+
"contenir que des listes de deux éléments (x, y).")
107+
axe_x.append(donnee[0])
108+
axe_y.append(donnee[1])
109+
axes = DataFrame({"x": axe_x, "y": axe_y})
83110
# Exportation
84111
try:
85-
with open(self._chemin_sortie, 'w', encoding="utf-8") as fichier:
86-
dump(donnees, fichier, indent=4)
112+
chemin_fichier = join(self._chemin_sortie, nom_fichier)
113+
camembert = Chart(axes).mark_arc().encode(
114+
theta='y:Q',
115+
color='x:N',
116+
tooltip=['x:N', 'y:Q']
117+
)
118+
camembert.save(chemin_fichier)
87119
except Exception as ex:
88-
raise ExportationException(str(ex)) from ex
120+
raise ExportationCamembertHtmlException("Erreur lors de l'exportation "
121+
f"du camembert {nom_fichier}.") from ex
122+
89123

90124
class ExportationException(Exception):
91125
"""
92126
Représente une erreur lors de l'exportation de données.
93127
"""
94128

95-
class ExportationDossierParentException(ExportationException):
129+
class ExportationJsonException(ExportationException):
130+
"""
131+
Représente une erreur lors de l'exportation de données vers un format JSON.
132+
"""
133+
134+
class ExportationCamembertHtmlException(ExportationException):
135+
"""
136+
Représente une erreur lors de l'exportation de données vers un histogramme
137+
au format HTML.
138+
"""
139+
140+
class ExportationDossierIntrouvableException(ExportationException):
96141
"""
97142
Représente une erreur lorsque une exportation est impossible
98-
lorsque le dossier parent du fichier d'exportation n'existe pas.
143+
lorsque le dossier de l'exportation n'existe pas.
99144
"""

app/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from analyse.analyseur_log_apache import AnalyseurLogApache
99
from export.exporteur import Exporteur, ExportationException
1010

11-
1211
def main() -> None:
1312
"""
1413
Point d'entrée de l'application.
@@ -32,9 +31,15 @@ def main() -> None:
3231
# Analyse statistique du fichier log
3332
analyseur_log = AnalyseurLogApache(fichier_log, filtre_log)
3433
analyse = analyseur_log.get_analyse_complete()
35-
# Exportation de l'analyse
34+
# Exportation JSON
3635
exporteur = Exporteur(arguments_cli.sortie)
37-
exporteur.export_vers_json(analyse)
36+
exporteur.export_vers_json(analyse, "analyse-log-apache.json")
37+
# Exportation Camembert
38+
if arguments_cli.camembert:
39+
exporteur.export_vers_html_camembert(
40+
analyseur_log.get_total_par_code_statut_http_camembert(),
41+
"camembert-code_statut_http.html"
42+
)
3843
# Termine l'animation de chargement
3944
afficheur_cli.stop_animation_chargement()
4045
except ArgumentCLIException as ex:

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def fichier_json(tmp_path):
193193
return fichier_temp
194194

195195
@pytest.fixture
196-
def exporteur(fichier_json):
196+
def exporteur(tmp_path):
197197
"""
198198
Fixture pour initialiser un exportateur de données.
199199
@@ -204,4 +204,4 @@ def exporteur(fichier_json):
204204
Returns:
205205
Exporteur: Une instance de la classe :class:`Exportateur`.
206206
"""
207-
return Exporteur(str(fichier_json))
207+
return Exporteur(str(tmp_path))

tests/test_analyseur_log_apache.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,29 @@ def test_analyseur_repartition_code_statut_http_valide(analyseur_log_apache):
218218
assert repartition[1]["total"] == 1
219219
assert repartition[1]["taux"] == 20.0
220220

221+
def test_analyseur_repartition_code_statut_http_camembert_valide(analyseur_log_apache):
222+
"""
223+
Vérifie que ``get_total_par_code_statut_http_camembert`` retourne la répartition
224+
correcte des codes HTTP.
225+
226+
Scénarios testés:
227+
- Vérification du tri et des indexs des listes dans la liste.
228+
229+
Asserts:
230+
- La liste est triée dans l'ordre attendu.
231+
- Le nombre d'éléments dans le résultat correspond à celui attendu.
232+
233+
Args:
234+
analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance
235+
de la classe ParseurLogApache.
236+
"""
237+
repartition = analyseur_log_apache.get_total_par_code_statut_http_camembert()
238+
assert len(repartition) == 2
239+
assert repartition[0][0] == 500
240+
assert repartition[0][1] == 4
241+
assert repartition[1][0] == 200
242+
assert repartition[1][1] == 1
243+
221244
@pytest.mark.parametrize("nombre_entrees", [
222245
(0), (3), (100)
223246
])

0 commit comments

Comments
 (0)