Skip to content
Merged
2 changes: 2 additions & 0 deletions .github/workflows/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
pip install sphinx
pip install sphinx_rtd_theme --break-system-packages
pip install colorama
pip install altair
pip install pandas

# Étape 4 : Générer la documentation
- name: Construction de la documentation (avec Sphinx)
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/qualite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
python -m pip install --upgrade pip
pip install pylint
pip install colorama
pip install altair
pip install pandas

# Étape 4 : Lancement de l'analyse
- name: Analyse avec Pylint (note >= 9.0 requise)
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install colorama
pip install altair
pip install pandas
pip install pytest
pip install pytest-cov
pip install pytest-mock
Expand Down
17 changes: 16 additions & 1 deletion app/analyse/analyseur_log_apache.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def get_top_urls(self) -> list:

def get_total_par_code_statut_http(self) -> list:
"""
Retourne la répartition des réponses par code de statut htpp retourné.
Retourne la répartition des réponses par code de statut http retourné.
Les entrées prisent en compte sont uniquement celles qui ont passées le filtre.

Returns:
Expand All @@ -192,3 +192,18 @@ def get_total_par_code_statut_http(self) -> list:
[entree.reponse.code_statut_http for entree in self._get_entrees_passent_filtre()],
"code"
)

def get_total_par_code_statut_http_camembert(self) -> list:
"""
Retourne la répartition des réponses par code de statut http retourné sous
un format utilisable par un camembert.
Les entrées prisent en compte sont uniquement celles qui ont passées le filtre.

Returns:
list: Une liste de liste de deux éléments où l'index 0 est le code et l'index 1
son total d'apparition.
"""
return [
[stat["code"], stat["total"]]
for stat in self.get_total_par_code_statut_http()
]
16 changes: 8 additions & 8 deletions app/cli/parseur_arguments_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def __set_arguments(self) -> None:
"-s",
"--sortie",
type=str,
default="./analyse-log-apache.json",
help="Fichier JSON où sera écrit l'analyse. Par défaut, un fichier avec le "
"nom 'analyse-log-apache.json' dans le repertoire courant sera crée.",
default="./",
help="Dossier où sera écrit l'analyse du fichier de log Apache. Par défaut,"
"sa valeur est le répertoire d'exécution du script.",
)
self.add_argument(
"-i",
Expand All @@ -56,6 +56,11 @@ def __set_arguments(self) -> None:
type=int,
help="Le code de statut http que doivent avoir les entrées à analyser."
)
self.add_argument(
"--camembert",
action="store_true",
help="Active la génération d'histogrammes pour les statistiques compatibles."
)

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

if not arguments_parses.sortie.endswith(".json"):
raise ArgumentCLIException(
"Le fichier de sortie doit obligatoirement être un fichier au format json."
)

return arguments_parses


Expand Down
143 changes: 94 additions & 49 deletions app/export/exporteur.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
Module pour l'exportation des données.
"""

from os.path import abspath, dirname, isdir
from os.path import abspath, isdir, join
from json import dump
from altair import Chart
from pandas import DataFrame


class Exporteur:
Expand All @@ -12,88 +14,131 @@ class Exporteur:
vers un fichier de sortie.

Attributes:
_chemin_sortie (str): Le chemin du fichier vers lequel
les données seront exportées.
_chemin_sortie (str): Le chemin du dossier vers lequel les données
vont être exportées.
"""

def __init__(self, chemin_sortie: str):
"""
Initialise un exporteur de données.

Args:
chemin_sortie (str): Le chemin du fichier vers lequel
les données seront exportées.

Raises:
TypeError: Le chemin de sortie n'est pas une chaîne de caractère.
ExportationDossierParentException: Exportation impossible à cause de
l'inexistance du dossier parent du fichier d'exportation.
"""
# Vérification du type du paramètre
# Vérification du paramètre
if not isinstance(chemin_sortie, str):
raise TypeError("Le chemin de sortie doit être une chaîne de caractère.")
# Vérification du chemin d'exportation
self.verification_exportation_possible(chemin_sortie)
# Ajout du chemin d'exportation
raise TypeError("Le chemin de sortie doit être une chaîne de caractères.")
# Vérification du chemin
chemin_sortie_absolue = abspath(chemin_sortie)
if not isdir(chemin_sortie_absolue):
raise ExportationDossierIntrouvableException(f"Impossible d'exporter vers le "
f"dossier {chemin_sortie} ({chemin_sortie_absolue}), "
"le dossier n'existe pas.")
# Ajout du chemin
self._chemin_sortie = chemin_sortie

def verification_exportation_possible(self, chemin_sortie: str) -> None:
def export_vers_json(self, donnees: dict, nom_fichier: str) -> None:
"""
Vérifie qu'une exportation est possible vers le chemin du fichier indiqué. Renvoie une
exception expliquant le problème si elle n'est pas possible.
Export le dictionnaire fourni vers le ``chemin de sortie``.

Args:
chemin_sortie (str): Le chemin du fichier d'exportation.
donnees (dict): Le dictionnaire qui contient les données.
nom_fichier (str): Le nom du fichier JSON.

Returns:
None

Raises:
ExportationDossierParentException: Le dossier parent du fichier n'existe pas.
TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire.
ExportationJsonException: Une erreur lors de l'écriture dans le fichier JSON.
"""
# Vérification du type du paramètre
if not isinstance(chemin_sortie, str):
raise TypeError("Le chemin de sortie doit être une chaîne de caractères.")
# Vérification du chemin
chemin_sortie_absolue = abspath(chemin_sortie)
dossier_parent = dirname(chemin_sortie_absolue)
if not isdir(dossier_parent):
raise ExportationDossierParentException(f"Impossible d'exporter vers le "
f"fichier {chemin_sortie}, son dossier parent "
f"{dossier_parent} n'existe pas.")
# Vérification du type des paramètres
if not isinstance(donnees, dict):
raise TypeError("Les statistiques à exporter doivent être sous une forme "
"de dictionnaire.")
if not isinstance(nom_fichier, str):
raise TypeError("Le nom du fichier doit être une chaîne de caractère.")
# Vérification du nom du fichier
if not nom_fichier.endswith(".json"):
raise ValueError("Le fichier JSON doit terminé par l'extention '.json'.")
# Exportation
chemin_fichier = join(self._chemin_sortie, nom_fichier)
try:
with open(chemin_fichier, 'w', encoding="utf-8") as fichier:
dump(donnees, fichier, indent=4)
except Exception as ex:
raise ExportationJsonException(str(ex)) from ex

def export_vers_json(self, donnees: dict) -> None:
def export_vers_html_camembert(self,
donnees: list,
nom_fichier: str) -> None:
"""
Export le dictionnaire fourni vers le :attr:`chemin de sortie`.
Export la liste fournie vers un camembert HTML vers le ``chemin de sortie``.

Args:
donnees (dict): Le dictionnaire qui contient les données.
donnees (list): Les données du camembert. La liste doit contenir
des listes de deux éléments où le premier reprèsente le nom de cette
partie du camembert et le deuxième sa valeur.
nom_fichier (str): Le nom du fichier HTML.

Returns:
None

Raises:
TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire.
ExportationException: Une erreur lors de l'écriture dans le fichier JSON.
TypeError: Les paramètres ne sont pas du type attendu ou la liste ``donnees``
contient un élément qui n'est pas une liste.
ValueError: Le paramètre ``nom_fichier`` ne termine pas par .html ou le paramètre
``donnees`` ne contient pas des listes de longueur 2.
ExportationCamembertHtmlException: Erreur lors de l'exportation du camembert.
"""
# Vérification du type du paramètre
if not isinstance(donnees, dict):
raise TypeError("Les données à exporter doivent être sous une forme "
"de dictionnaire.")
# Vérification du type des paramètres
if not isinstance(donnees, list):
raise TypeError("Les données de l'histogramme à exporter doit être sous une forme "
"de liste.")
if not isinstance(nom_fichier, str):
raise TypeError("Le nom du fichier doit être une chaîne de caractère.")
# Vérification du nom du fichier
if not nom_fichier.endswith(".html"):
raise ValueError("Le fichier HTML doit terminé par l'extention '.html'.")
# Récupération des axes du graphique
axe_x = []
axe_y = []
for donnee in donnees:
if not isinstance(donnee, list):
raise ValueError("La liste des données de l'histogramme à exporter ne doit "
"contenir que des listes.")
if not len(donnee) == 2:
raise ValueError("La liste des données de l'histogramme à exporter ne doit "
"contenir que des listes de deux éléments (x, y).")
axe_x.append(donnee[0])
axe_y.append(donnee[1])
axes = DataFrame({"x": axe_x, "y": axe_y})
# Exportation
try:
with open(self._chemin_sortie, 'w', encoding="utf-8") as fichier:
dump(donnees, fichier, indent=4)
chemin_fichier = join(self._chemin_sortie, nom_fichier)
camembert = Chart(axes).mark_arc().encode(
theta='y:Q',
color='x:N',
tooltip=['x:N', 'y:Q']
)
camembert.save(chemin_fichier)
except Exception as ex:
raise ExportationException(str(ex)) from ex
raise ExportationCamembertHtmlException("Erreur lors de l'exportation "
f"du camembert {nom_fichier}.") from ex


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

class ExportationDossierParentException(ExportationException):
class ExportationJsonException(ExportationException):
"""
Représente une erreur lors de l'exportation de données vers un format JSON.
"""

class ExportationCamembertHtmlException(ExportationException):
"""
Représente une erreur lors de l'exportation de données vers un histogramme
au format HTML.
"""

class ExportationDossierIntrouvableException(ExportationException):
"""
Représente une erreur lorsque une exportation est impossible
lorsque le dossier parent du fichier d'exportation n'existe pas.
lorsque le dossier de l'exportation n'existe pas.
"""
11 changes: 8 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from analyse.analyseur_log_apache import AnalyseurLogApache
from export.exporteur import Exporteur, ExportationException


def main() -> None:
"""
Point d'entrée de l'application.
Expand All @@ -32,9 +31,15 @@ def main() -> None:
# Analyse statistique du fichier log
analyseur_log = AnalyseurLogApache(fichier_log, filtre_log)
analyse = analyseur_log.get_analyse_complete()
# Exportation de l'analyse
# Exportation JSON
exporteur = Exporteur(arguments_cli.sortie)
exporteur.export_vers_json(analyse)
exporteur.export_vers_json(analyse, "analyse-log-apache.json")
# Exportation Camembert
if arguments_cli.camembert:
exporteur.export_vers_html_camembert(
analyseur_log.get_total_par_code_statut_http_camembert(),
"camembert-code_statut_http.html"
)
# Termine l'animation de chargement
afficheur_cli.stop_animation_chargement()
except ArgumentCLIException as ex:
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def fichier_json(tmp_path):
return fichier_temp

@pytest.fixture
def exporteur(fichier_json):
def exporteur(tmp_path):
"""
Fixture pour initialiser un exportateur de données.

Expand All @@ -204,4 +204,4 @@ def exporteur(fichier_json):
Returns:
Exporteur: Une instance de la classe :class:`Exportateur`.
"""
return Exporteur(str(fichier_json))
return Exporteur(str(tmp_path))
23 changes: 23 additions & 0 deletions tests/test_analyseur_log_apache.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,29 @@ def test_analyseur_repartition_code_statut_http_valide(analyseur_log_apache):
assert repartition[1]["total"] == 1
assert repartition[1]["taux"] == 20.0

def test_analyseur_repartition_code_statut_http_camembert_valide(analyseur_log_apache):
"""
Vérifie que ``get_total_par_code_statut_http_camembert`` retourne la répartition
correcte des codes HTTP.

Scénarios testés:
- Vérification du tri et des indexs des listes dans la liste.

Asserts:
- La liste est triée dans l'ordre attendu.
- Le nombre d'éléments dans le résultat correspond à celui attendu.

Args:
analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance
de la classe ParseurLogApache.
"""
repartition = analyseur_log_apache.get_total_par_code_statut_http_camembert()
assert len(repartition) == 2
assert repartition[0][0] == 500
assert repartition[0][1] == 4
assert repartition[1][0] == 200
assert repartition[1][1] == 1

@pytest.mark.parametrize("nombre_entrees", [
(0), (3), (100)
])
Expand Down
Loading