From f0b54843438a113af6e5a081041d6ebb091987a0 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Fri, 21 Mar 2025 20:44:37 +0100 Subject: [PATCH 01/47] =?UTF-8?q?Docs=20:=20Mise=20=C3=A0=20jour=20du=20RE?= =?UTF-8?q?ADME.md=20-=20Mise=20=C3=A0=20jour=20du=20titre=20du=20projet?= =?UTF-8?q?=20-=20Ajout=20d'une=20courte=20description=20du=20projet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d28618..0158b45 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# code_source -C - Analysateur de fichiers logs +# LogBuster + +Bienvenue dans le monde de LogBuster, l'outil ultime pour analyser, décortiquer et sauver vos logs Apache des griffes du chaos. Vous avez des logs qui traînent, qui sont indéchiffrables ou tout simplement encombrants ? Pas de panique, LogBuster est là pour les attraper, les analyser et vous offrir des statistiques claires et précises, comme jamais auparavant ! + +Inspiré par l’univers de Ghostbusters, LogBuster chasse les erreurs et anomalies des logs Apache avec la même efficacité que les chasseurs de fantômes. Que ce soit pour des fichiers volumineux ou des logs complexes, LogBuster est votre équipe d’intervention spécialisée. Plus de confusion, plus de stress – vos logs sont en sécurité ! \ No newline at end of file From eb558436da619900ae060a1929a66027720de080 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Fri, 21 Mar 2025 21:10:55 +0100 Subject: [PATCH 02/47] =?UTF-8?q?Chore=20:=20Ajout=20du=20gitignore=20et?= =?UTF-8?q?=20d'=C3=A9l=C3=A9ments=20=C3=A0=20ignorer=20-=20Ignore=20l'env?= =?UTF-8?q?ironnement=20virtuel=20-=20Ignore=20les=20fichiers=20propres=20?= =?UTF-8?q?=C3=A0=20Python=20-=20Ignore=20les=20fichiers=20propres=20aux?= =?UTF-8?q?=20IDE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8455b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Environnement virtuel +.venv/ + +# Fichiers propres à Python +*.pyc +__pycache__/ + +# Fichiers propres à Python +.vscode/ +.idea/ \ No newline at end of file From 1c89f08f2ffd86690cc8910bbad87b955fb4b03e Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Fri, 21 Mar 2025 21:36:15 +0100 Subject: [PATCH 03/47] Feat : Ajout du fichier main.py - Initialisation du fichier principal de l'applicatin --- app/main.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/main.py diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f7888b2 --- /dev/null +++ b/app/main.py @@ -0,0 +1,2 @@ +if __name__ == "__main__": + print("Début de LogBoster !") \ No newline at end of file From d03b836e916eb3cf71c93e0527b9eff04fc709d2 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 09:01:37 +0100 Subject: [PATCH 04/47] Refractor : Ajout de la docstring du module main - Ajout de la docstring du module main.py --- app/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/main.py b/app/main.py index f7888b2..263251e 100644 --- a/app/main.py +++ b/app/main.py @@ -1,2 +1,6 @@ +""" +Point d'entrée de l'application LogBuster ! +""" + if __name__ == "__main__": print("Début de LogBoster !") \ No newline at end of file From 0e5819bdf32d2bcc79f657612f7b82a29be35acb Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 09:03:55 +0100 Subject: [PATCH 05/47] Feat : Ajout du logo de l'application au lancement - Ecriture du logo (format ASCII) de l'application dans la CLI depuis le fichier main.py au lancement de l'application --- app/main.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 263251e..966dcf6 100644 --- a/app/main.py +++ b/app/main.py @@ -2,5 +2,20 @@ Point d'entrée de l'application LogBuster ! """ +import colorama + + if __name__ == "__main__": - print("Début de LogBoster !") \ No newline at end of file + colorama.init() + print(colorama.Style.DIM + r""" + .-. .-') .-') .-') _ ('-. _ .-') ,---. + \ ( OO ) ( OO ). ( OO) ) _( OO)( \( -O ) | | + ,--. .-'),-----. ,----. ;-----.\ ,--. ,--. (_)---\_)/ '._(,------.,------. | | + | |.-') ( OO' .-. ' ' .-./-') | .-. | | | | | / _ | |'--...__)| .---'| /`. '| | + | | OO )/ | | | | | |_( O- )| '-' /_) | | | .-') \ :` `. '--. .--'| | | / | || | + | |`-' |\_) | |\| | | | .--, \| .-. `. | |_|( OO ) '..`''.) | | (| '--. | |_.' || .' +(| '---.' \ | | | |(| | '. (_/| | \ | | | | `-' /.-._) \ | | | .--' | . '.'`--' + | | `' '-' ' | '--' | | '--' /(' '-'(_.-' \ / | | | `---.| |\ \ .--. + `------' `-----' `------' `------' `-----' `-----' `--' `------'`--' '--''--' + + """) From c89df0f84e2304b4fd89a171dc6517be1c04f993 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:33:00 +0100 Subject: [PATCH 06/47] Feat: Ajout de l'analyse des arguments en CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Utilisation de la bibliothèque argparse - Création du fichier parseur_arguments_cli.py pour gérer les arguments en CLI - Implémentation de la classe ParseurArgumentsCLI pour analyser les arguments en CLI - Ajout de l'exception ArgumentCLIException pour capturer et gérer les erreurs de parsing des arguments en CLI - Mise à jour de main.py pour intégrer l'analyse des arguments en CLI --- app/cli/parseur_arguments_cli.py | 65 ++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 app/cli/parseur_arguments_cli.py diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py new file mode 100644 index 0000000..7a266b5 --- /dev/null +++ b/app/cli/parseur_arguments_cli.py @@ -0,0 +1,65 @@ +""" +Module pour analyser les arguments passés en ligne de commande. +""" + +from argparse import ArgumentParser + + +class ParseurArgumentsCLI(ArgumentParser): + """ + Représente un parseur pour analyser les arguments passés en ligne + de commande pour l'application. + """ + + def __init__(self): + super().__init__( + description="LogBuster, l'analyseur de log Apache.", allow_abbrev=False + ) + self.__set_arguments() + + def __set_arguments(self): + """ + Définit les arguments attendus par l'application. + """ + # -- Argument obligatoire -- + self.add_argument( + "chemin_log", + type=str, + help="Chemin du fichier log Apache à analyser." + ) + # -- Argument optionnel -- + self.add_argument( + "-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.", + ) + + def parse_args(self, args=None, namespace=None): + """ + Analyse, vérifie et retourne les arguments fournis en ligne de commande. + """ + # Analyse des arguments + try: + arguments_parses = super().parse_args(args, namespace) + except Exception as ex: + raise ArgumentCLIException(str(ex)) from ex + + # Vérification syntaxique des arguments + if not arguments_parses.sortie.endswith(".json"): + raise ArgumentCLIException( + "Le fichier de sortie doit obligatoirement être un fichier au format json." + ) + + return arguments_parses + + +class ArgumentCLIException(Exception): + """ + Représente une erreur liée l'analyse d'un argument en ligne de commande. + """ + + def __init__(self, *args): + super().__init__(*args) From 9f5f5dabc432323e8ec947e4e988b6623e4caf1d Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 10:08:09 +0100 Subject: [PATCH 07/47] Fix: Ajout de l'analyse des arguments CLI au lancement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mise à jour du fichier main.py pour intégrer l'analyse des arguments CLI qui a été oubliée lors de la précédente PR --- app/main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/main.py b/app/main.py index 966dcf6..abe024e 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ """ import colorama +from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException if __name__ == "__main__": @@ -19,3 +20,12 @@ `------' `-----' `------' `------' `-----' `-----' `--' `------'`--' '--''--' """) + try: + # Récupération des arguments + parseur_cli = ParseurArgumentsCLI() + arguments_cli = parseur_cli.parse_args() + # Analyse syntaxique du fichier log + # Analyse statistique du fichier log + # Exportation de l'analyse + except ArgumentCLIException as ex: + print(f"Erreur dans les arguments fournis !\n {ex}") From a8836185df68b5442ab363bca4d429ec64fc2e2c Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:59:04 +0100 Subject: [PATCH 08/47] =?UTF-8?q?Feat:=20V=C3=A9rification=20des=20chemins?= =?UTF-8?q?=20pass=C3=A9s=20en=20argument=20dans=20la=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Utilisation de la bibliothèque re - Vérification que le chemin du fichier log Apache ne contient que les caractères autorisés - Vérification que le chemin du fichier de sortie ne contient que les caractères autorisés --- app/cli/parseur_arguments_cli.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py index 7a266b5..20092c1 100644 --- a/app/cli/parseur_arguments_cli.py +++ b/app/cli/parseur_arguments_cli.py @@ -3,7 +3,7 @@ """ from argparse import ArgumentParser - +from re import match class ParseurArgumentsCLI(ArgumentParser): """ @@ -48,6 +48,19 @@ def parse_args(self, args=None, namespace=None): raise ArgumentCLIException(str(ex)) from ex # Vérification syntaxique des arguments + regex_chemin = r"^[a-zA-Z0-9_\\\-.\/]+$" + if not match(regex_chemin, arguments_parses.chemin_log): + raise ArgumentCLIException( + "Le chemin du fichier log doit uniquement contenir les caractères autorisés. " + "Les caractères autorisés sont les minuscules, majuscules, chiffres ou les " + "caractères spéciaux suivants: _, \\, -, /." + ) + if not match(regex_chemin, arguments_parses.sortie): + raise ArgumentCLIException( + "Le chemin du fichier de sortie doit uniquement contenir les caractères " + "autorisés. Les caractères autorisés sont les minuscules, majuscules, " + "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." From b5e263552af54a9d28f44f6190f3eb7f7248851e Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:00:43 +0100 Subject: [PATCH 09/47] =?UTF-8?q?Fix:=20Modifiation=20de=20la=20v=C3=A9rif?= =?UTF-8?q?ication=20des=20chemins=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout du caractère ':' dans les caractères autorisés dans les chemins fournis par la CLI afin d'autoriser les chemins commençant par C: ou D: --- app/cli/parseur_arguments_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py index 20092c1..8411e86 100644 --- a/app/cli/parseur_arguments_cli.py +++ b/app/cli/parseur_arguments_cli.py @@ -48,7 +48,7 @@ def parse_args(self, args=None, namespace=None): raise ArgumentCLIException(str(ex)) from ex # Vérification syntaxique des arguments - regex_chemin = r"^[a-zA-Z0-9_\\\-.\/]+$" + regex_chemin = r"^[a-zA-Z0-9:_\\\-.\/]+$" if not match(regex_chemin, arguments_parses.chemin_log): raise ArgumentCLIException( "Le chemin du fichier log doit uniquement contenir les caractères autorisés. " From be1a88882a86313597aa9eb657cbfdafa2066847 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:48:11 +0100 Subject: [PATCH 10/47] Test: Ajout des tests unitaires pour ParseurArgumentsCLI (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création du dossier tests où seront rangés les tests unitaires - Ajout des tests unitaires pour ParseurArgumentsCLI afin de vérifier que l'objet parse, vérifie et retourne les arguments correctement --- tests/test_parseur_arguments_cli.py | 121 ++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tests/test_parseur_arguments_cli.py diff --git a/tests/test_parseur_arguments_cli.py b/tests/test_parseur_arguments_cli.py new file mode 100644 index 0000000..1f15147 --- /dev/null +++ b/tests/test_parseur_arguments_cli.py @@ -0,0 +1,121 @@ +""" +Module des tests unitaires pour le parseur des arguments passés depuis la CLI. +""" + +import pytest +from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException + +# Données utilisées pour les tests unitaires + +@pytest.fixture +def parseur_arguments_cli(): + """ + Fixture pour initialiser le parseur d'arguments CLI. + Retourne une instance de la classe ParseurArgumentsCLI pour être utilisée dans les tests. + Returns: + ParseurArgumentsCLI: Une instance de la classe ParseurArgumentsCLI. + """ + return ParseurArgumentsCLI() + +chemins_valides = [ + "fichier.log", + "f1chier.txt", + "./fichier.log", + "C:\\Users\\fest\\gros_fichier.log" +] + +chemins_invalides = [ + "", + "f$chier#2.log", + "fichier txt" +] + +sorties_valides = [ + "fichier.json", + "./fichier.json", + "C:/Users/fest/fichier.json" +] + +# Tests unitaires + +@pytest.mark.parametrize("chemin_log", chemins_valides) +def test_recuperation_chemin_log_valide(parseur_arguments_cli, chemin_log): + """ + Vérifie que le chemin du log Apache fourni depuis la ligne de commande est bien + récupéré par le parseur. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurArgumentsCLI. + chemin_log (str): Un chemin de fichier valide. + Returns: + None + """ + arguments = parseur_arguments_cli.parse_args(args=[chemin_log]) + assert arguments.chemin_log == chemin_log + +@pytest.mark.parametrize("chemin_log", chemins_invalides) +def test_exception_chemin_log_invalide(parseur_arguments_cli, chemin_log): + """ + Vérifie qu'une erreur est retournée lorsque le chemin du log Apache fourni contient + au moins un caractère non autorisé. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurArgumentsCLI. + chemin_log (str): Un chemin de fichier invalide. + Returns: + None + """ + with pytest.raises(ArgumentCLIException): + parseur_arguments_cli.parse_args(args=[chemin_log]) + +@pytest.mark.parametrize("chemin_sortie", sorties_valides) +def test_recuperation_chemin_sortie_valide(parseur_arguments_cli, chemin_sortie): + """ + Vérifie que le chemin du fichier de sortie JSON fourni depuis la ligne de commande est bien + récupéré par le parseur. + Args: + parseur_arguments_cli: Fixture pour l'instance de la classe ParseurArgumentsCLI + Returns: + None + """ + arguments = parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", chemin_sortie]) + assert arguments.sortie == chemin_sortie + +@pytest.mark.parametrize("chemin_sortie", chemins_invalides) +def test_exception_chemin_sortie_invalide(parseur_arguments_cli, chemin_sortie): + """ + Vérifie qu'une erreur est retournée lorsque le chemin du fichier de sortie fourni contient + au moins un caractère non autorisé. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurArgumentsCLI. + chemin_sortie (str): Un chemin de fichier invalide. + Returns: + None + """ + with pytest.raises(ArgumentCLIException): + parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", chemin_sortie]) + +def test_recuperation_chemin_sortie_defaut_valide(parseur_arguments_cli): + """ + Vérifie que le chemin du fichier de sortie JSON par défaut est bien appliqué lorsque + aucun chemin de sortie n'est donné. + Args: + parseur_arguments_cli: Fixture pour l'instance de la classe ParseurArgumentsCLI + Returns: + None + """ + argument = parseur_arguments_cli.parse_args(args=["fichier.txt"]) + assert argument.sortie == "./analyse-log-apache.json" + +def test_verification_extention_chemin_sortie(parseur_arguments_cli): + """ + Vérifie qu'une erreur est retournée lorsque le fichier de sortie fourni ne possède + pas l'extension '.json'. + Args: + parseur_arguments_cli: Fixture pour l'instance de la classe ParseurArgumentsCLI + Returns: + None + """ + with pytest.raises(ArgumentCLIException): + parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", "invalide.txt"]) From 5d9e98f3d2384acd89652d86432796d05155fe84 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 15:01:12 +0100 Subject: [PATCH 11/47] Test: Ajout du fichier pytest.ini (#7) - Mise en place du fichier pytest.ini afin de faciliter les imports des modules du dossier /app depuis les tests du dossier /test --- tests/pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/pytest.ini diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..520e5c5 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = ../app \ No newline at end of file From 414293c5561000912c83cc53d2bc59a33638e029 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 16:01:35 +0100 Subject: [PATCH 12/47] Chore: Modification du .gitignore pour les tests unitaires (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ignore les fichiers propres à Pytest - Ignore les fichiers propres à Coverage --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d8455b7..e363721 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,11 @@ __pycache__/ # Fichiers propres à Python .vscode/ -.idea/ \ No newline at end of file +.idea/ + +# Fichiers propres à Pytest +.pytest_cache/ + +# Fichiers propres à Coverage +.coverage/ +htmlcov/ \ No newline at end of file From 6cbc77bb2c377e0a7a6d43eea1229cab64e86c97 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 16:46:06 +0100 Subject: [PATCH 13/47] Feat: Ajout d'un message en cas d'argument invalide (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prise en compte de l'exception SystemExit dans la méthode parse_args afin de renvoyé une exception ArgumentCLIException pour afficher un message personnalisé --- app/cli/parseur_arguments_cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py index 8411e86..47859a5 100644 --- a/app/cli/parseur_arguments_cli.py +++ b/app/cli/parseur_arguments_cli.py @@ -13,7 +13,7 @@ class ParseurArgumentsCLI(ArgumentParser): def __init__(self): super().__init__( - description="LogBuster, l'analyseur de log Apache.", allow_abbrev=False + description="LogBuster, l'analyseur de log Apache.", allow_abbrev=False, ) self.__set_arguments() @@ -44,8 +44,10 @@ def parse_args(self, args=None, namespace=None): # Analyse des arguments try: arguments_parses = super().parse_args(args, namespace) - except Exception as ex: + except Exception as ex: #Erreurs liées au parsing raise ArgumentCLIException(str(ex)) from ex + except SystemExit as ex: #Arguments inconnus + raise ArgumentCLIException() from ex # Vérification syntaxique des arguments regex_chemin = r"^[a-zA-Z0-9:_\\\-.\/]+$" From d667997f2e50dfcbb791837544daa55a1e22fbb2 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:04:15 +0100 Subject: [PATCH 14/47] =?UTF-8?q?Fix:=20Suppression=20d'une=20v=C3=A9rific?= =?UTF-8?q?ation=20incorrecte=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Suppression d'un except Exception dans parse_args de ParseurArgumentsCLI qui était inutile car l'exception ne peut pas être levé puisque la méthode ne lève que des exceptions de type SystemExit --- app/cli/parseur_arguments_cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py index 47859a5..030f3db 100644 --- a/app/cli/parseur_arguments_cli.py +++ b/app/cli/parseur_arguments_cli.py @@ -44,8 +44,6 @@ def parse_args(self, args=None, namespace=None): # Analyse des arguments try: arguments_parses = super().parse_args(args, namespace) - except Exception as ex: #Erreurs liées au parsing - raise ArgumentCLIException(str(ex)) from ex except SystemExit as ex: #Arguments inconnus raise ArgumentCLIException() from ex From 01d3287eb8b440470aca81e9f37b7134e369ee43 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:15:45 +0100 Subject: [PATCH 15/47] Test: Ajout d'un test unitaire pour ParseurArgumentsCLI (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout d'une fonction afin de tester qu'un exception est bien renvoyée dans le cas où un argument est passé par la CLI mais qu'il n'est pas reconnu par le parseur. --- tests/test_parseur_arguments_cli.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_parseur_arguments_cli.py b/tests/test_parseur_arguments_cli.py index 1f15147..08a83cf 100644 --- a/tests/test_parseur_arguments_cli.py +++ b/tests/test_parseur_arguments_cli.py @@ -36,8 +36,30 @@ def parseur_arguments_cli(): "C:/Users/fest/fichier.json" ] +arguments_invalides = [ + ["fichier.txt", "-s"], + ["fichier.txt", "-f", "inutile"], + ["fichier.txt", "--faux", "inutile"], + ["fichier.txt", "--faux", "inutile", "-d"], +] + # Tests unitaires +@pytest.mark.parametrize("arguments", arguments_invalides) +def test_exception_argument_inconnu(parseur_arguments_cli, arguments): + """ + Vérifie qu'une erreur est retournée lorsque un argument est passé en CLI et qu'il + n'est pas reconnu. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurArgumentsCLI. + chemin_log (str): Un chemin de fichier valide. + Returns: + None + """ + with pytest.raises(ArgumentCLIException): + arguments = parseur_arguments_cli.parse_args(args=arguments) + @pytest.mark.parametrize("chemin_log", chemins_valides) def test_recuperation_chemin_log_valide(parseur_arguments_cli, chemin_log): """ From 63c8223cc688f1bdec38a79d948514d8f6b65452 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:38:56 +0100 Subject: [PATCH 16/47] Feat: Ajout du parseur pour les logs Apache (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de la classe ParseurLogApache - Vérification que le fichier log existe lors de l'initialisation de l'objet ParseurLogApache - Affichage d'un message lorsque le fichier log n'existe pas --- app/parse/parseur_log_apache.py | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 app/parse/parseur_log_apache.py diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py new file mode 100644 index 0000000..7212caa --- /dev/null +++ b/app/parse/parseur_log_apache.py @@ -0,0 +1,46 @@ +""" +Module pour parser un fichier log Apache. +""" + +import os +from re import match, compile +from datetime import datetime + + +class ParseurLogApache(): + """ + Représente un parseur pour faire une analyse synthaxique d'un fichier + log Apache. + Attributes: + PATTERN_ENTREE_LOG_APACHE (str): Le pattern regex d'une entrée dans un log Apache. + """ + + PATTERN_ENTREE_LOG_APACHE = ( + r'(?P\S+) (?P\S+) (?P\S+)' + r' (\[(?P.+?)\]|-) "((?P\S+) (?P\S+) (?P\S+)|-)"' + r' (?P\d+) (?P\d+|-)' + r'( "(?P.*?)" "(?P.*?)")?' + ) + + def __init__(self, chemin_log): + """ + Initialise un nouveau parseur de fichier log Apache et vérifie que + le fichier passé en paramètre existe. + Args: + chemin_log (str): Le chemin du fichier à analyser. + Raises: + FileNotFoundError: Si le fichier à analyser est introuvable. + """ + if not self.__fichier_existe(chemin_log): + raise FileNotFoundError(f"Le fichier {chemin_log} est introuvable.") + self.chemin_log = chemin_log + + def __fichier_existe(self, chemin_fichier): + """ + Vérifie que le chemin passé en paramètre correspond à une fichier existant. + Returns: + bool: True s'il existe, False sinon. + """ + if not os.path.isfile(chemin_fichier): + return False + return True \ No newline at end of file From d409680454d34c6e845d41a4aee2268803b96cb0 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 19:58:23 +0100 Subject: [PATCH 17/47] =?UTF-8?q?Feat:=20Ajout=20de=20la=20classe=20Fichie?= =?UTF-8?q?rLogApache=20-=20Ajout=20de=20la=20classe=20FichierLogApache=20?= =?UTF-8?q?qui=20est=20une=20classe=20qui=20repr=C3=A9sente=20=20=20un=20f?= =?UTF-8?q?ichier=20de=20log=20Apache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/parse/fichier_log_apache.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/parse/fichier_log_apache.py diff --git a/app/parse/fichier_log_apache.py b/app/parse/fichier_log_apache.py new file mode 100644 index 0000000..5e044c5 --- /dev/null +++ b/app/parse/fichier_log_apache.py @@ -0,0 +1,23 @@ +""" +Module qui contient les classes pour représenter un fichier log Apache. +""" + + +class FichierLogApache: + """ + Représente un fichier de log Apache. + """ + + def __init__(self, chemin): + self.chemin = chemin + self.entrees = [] + + def ajoute_entree(self, entree): + """ + Ajoute une entrée à la liste des entrées du fichier. + Args: + entree (EntreeLogApache): L'entrée à ajouter. + Returns: + None + """ + self.entrees.append(entree) \ No newline at end of file From 087f4e3ebdafbbb71415e6ce2bfa6df042a6503b Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 20:14:03 +0100 Subject: [PATCH 18/47] =?UTF-8?q?Feat:=20Ajout=20de=20EntreeLogApache=20-?= =?UTF-8?q?=20Ajout=20de=20EntreeLogApache=20qui=20est=20une=20classe=20qu?= =?UTF-8?q?i=20repr=C3=A9sente=20une=20entr=C3=A9e=20=20=20dans=20un=20fic?= =?UTF-8?q?hier=20de=20log=20Apache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/parse/fichier_log_apache.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/parse/fichier_log_apache.py b/app/parse/fichier_log_apache.py index 5e044c5..49b87c8 100644 --- a/app/parse/fichier_log_apache.py +++ b/app/parse/fichier_log_apache.py @@ -20,4 +20,19 @@ def ajoute_entree(self, entree): Returns: None """ - self.entrees.append(entree) \ No newline at end of file + self.entrees.append(entree) + + + +class EntreeLogApache: + """ + Représente une entrée dans un fichier de log Apache. + """ + def __init__(self, + informations_client, + informations_requete, + informations_reponse + ): + self.client = informations_client + self.requete = informations_requete + self.reponse = informations_reponse \ No newline at end of file From 9e2877901d022971c04bfb5e5e1e788ef75e6087 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 20:30:16 +0100 Subject: [PATCH 19/47] =?UTF-8?q?Feat:=20Ajout=20de=20ClientInformations?= =?UTF-8?q?=20-=20Ajout=20de=20ClientInformations=20qui=20est=20une=20clas?= =?UTF-8?q?se=20qui=20repr=C3=A9sente=20les=20=20=20informations=20du=20cl?= =?UTF-8?q?ient=20dans=20une=20entr=C3=A9e=20d'un=20fichier=20de=20log=20A?= =?UTF-8?q?pache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/donnees/client_informations.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/donnees/client_informations.py diff --git a/app/donnees/client_informations.py b/app/donnees/client_informations.py new file mode 100644 index 0000000..09a8c6c --- /dev/null +++ b/app/donnees/client_informations.py @@ -0,0 +1,17 @@ +""" +Module relatif aux informations d'un client dans un fichier de log Apache. +""" + + +class ClientInformations: + + def __init__(self, + adresse_ip, + identifiant_rfc, + nom_utilisateur, + agent_utilisateur + ): + self.adresse_ip = adresse_ip + self.identifiant_rfc = identifiant_rfc + self.nom_utilisateur = nom_utilisateur + self.agent_utilisateur = agent_utilisateur \ No newline at end of file From c3f7d0b8ce9321fd345cd5424d8061a305d34175 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 20:36:55 +0100 Subject: [PATCH 20/47] =?UTF-8?q?Feat:=20Ajout=20de=20RequeteInformations?= =?UTF-8?q?=20-=20Ajout=20de=20RequeteInformations=20qui=20est=20une=20cla?= =?UTF-8?q?sse=20qui=20repr=C3=A9sente=20les=20=20=20informations=20propre?= =?UTF-8?q?s=20=C3=A0=20la=20requ=C3=AAte=20dans=20une=20entr=C3=A9e=20d'u?= =?UTF-8?q?n=20fichier=20de=20log=20=20=20Apache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/donnees/requete_informations.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/donnees/requete_informations.py diff --git a/app/donnees/requete_informations.py b/app/donnees/requete_informations.py new file mode 100644 index 0000000..13fb74f --- /dev/null +++ b/app/donnees/requete_informations.py @@ -0,0 +1,19 @@ +""" +Module relatif aux informations de la requete dans un fichier de log Apache. +""" + + +class RequeteInformations: + + def __init__(self, + horodatage, + methode_http, + url, + protocole_http, + ancienne_url + ): + self.horodatage = horodatage + self.methode_http = methode_http + self.url = url + self.protocole_http = protocole_http + self.ancienne_url = ancienne_url \ No newline at end of file From 1404345735c258e02897ac6dd3110c91d0aba600 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 20:42:15 +0100 Subject: [PATCH 21/47] =?UTF-8?q?Feat:=20Ajout=20de=20ReponseInformations?= =?UTF-8?q?=20-=20Ajout=20de=20ReponseInformations=20qui=20est=20une=20cla?= =?UTF-8?q?sse=20qui=20repr=C3=A9sente=20les=20=20=20informations=20de=20l?= =?UTF-8?q?a=20r=C3=A9ponse=20dans=20une=20entr=C3=A9e=20d'un=20fichier=20?= =?UTF-8?q?de=20log=20Apache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/donnees/reponse_informations.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/donnees/reponse_informations.py diff --git a/app/donnees/reponse_informations.py b/app/donnees/reponse_informations.py new file mode 100644 index 0000000..f50ed72 --- /dev/null +++ b/app/donnees/reponse_informations.py @@ -0,0 +1,13 @@ +""" +Module relatif aux informations de la réponse dans un fichier de log Apache. +""" + + +class ReponseInformations: + + def __init__(self, + code_status_http, + taille_octets + ): + self.code_status_http = code_status_http + self.taille_octets = taille_octets From 5058f4c815bc34b5b1863954d7c9a197a9c09bbf Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 22 Mar 2025 22:02:50 +0100 Subject: [PATCH 22/47] =?UTF-8?q?Feat:=20Ajout=20de=20l'analyse=20syntaxiq?= =?UTF-8?q?ue=20de=20fichier=20log=20-=20Ajout=20de=20la=20m=C3=A9thode=20?= =?UTF-8?q?parse=5Ffichier=20de=20ParseurLogApache=20qui=20permet=20de=20?= =?UTF-8?q?=20parser=20un=20fichier=20de=20log=20Apache=20-=20Ajout=20de?= =?UTF-8?q?=20la=20m=C3=A9thode=20parse=5Fentree=20de=20ParseurLogApache?= =?UTF-8?q?=20qui=20permet=20de=20parser=20une=20entr=C3=A9e=20d'un=20fich?= =?UTF-8?q?ier=20de=20log=20Apache=20-=20Ajout=20de=20get=5Finformation=5F?= =?UTF-8?q?entree=20de=20ParseurLogApache=20qui=20permet=20de=20=20=20r?= =?UTF-8?q?=C3=A9cup=C3=A9rer=20la=20valeur=20d'une=20information=20dans?= =?UTF-8?q?=20une=20entr=C3=A9e=20-=20Ajout=20de=20FormatLogApacheInvalide?= =?UTF-8?q?Exception=20qui=20repr=C3=A9sente=20une=20erreur=20=20=20lors?= =?UTF-8?q?=20de=20l'analyse=20syntaxique=20d'un=20fichier=20log=20-=20Mod?= =?UTF-8?q?ification=20du=20fichier=20main.py=20pour=20appeller=20la=20fon?= =?UTF-8?q?ction=20parse=5Fentree=20=20=20et=20pour=20afficher=20un=20mess?= =?UTF-8?q?age=20en=20cas=20d'erreur=20dans=20l'analyse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 7 ++ app/parse/parseur_log_apache.py | 114 ++++++++++++++++++++++++++++++-- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index abe024e..f0467cd 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ import colorama from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException +from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException if __name__ == "__main__": @@ -25,7 +26,13 @@ parseur_cli = ParseurArgumentsCLI() arguments_cli = parseur_cli.parse_args() # Analyse syntaxique du fichier log + parseur_log = ParseurLogApache(arguments_cli.chemin_log) + parseur_log.parse_fichier() # Analyse statistique du fichier log # Exportation de l'analyse except ArgumentCLIException as ex: print(f"Erreur dans les arguments fournis !\n {ex}") + except FileNotFoundError as ex: + print(f"Erreur dans la recherche du log Apache !\n{ex}") + except FormatLogApacheInvalideException as ex: + print(f"Erreur dans l'analyse du log Apache !\n{ex}") diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py index 7212caa..557d4ed 100644 --- a/app/parse/parseur_log_apache.py +++ b/app/parse/parseur_log_apache.py @@ -3,8 +3,12 @@ """ import os -from re import match, compile +from re import match from datetime import datetime +from parse.fichier_log_apache import FichierLogApache, EntreeLogApache +from donnees.client_informations import ClientInformations +from donnees.requete_informations import RequeteInformations +from donnees.reponse_informations import ReponseInformations class ParseurLogApache(): @@ -16,10 +20,10 @@ class ParseurLogApache(): """ PATTERN_ENTREE_LOG_APACHE = ( - r'(?P\S+) (?P\S+) (?P\S+)' - r' (\[(?P.+?)\]|-) "((?P\S+) (?P\S+) (?P\S+)|-)"' - r' (?P\d+) (?P\d+|-)' - r'( "(?P.*?)" "(?P.*?)")?' + r'(?P\S+) (?P\S+) (?P\S+)' + r' (\[(?P.+?)\]|-) "((?P\S+) (?P\S+) (?P\S+)|-)"' + r' (?P\d+) (?P\d+|-)' + r'( "(?P.*?)" "(?P.*?)")?' ) def __init__(self, chemin_log): @@ -43,4 +47,102 @@ def __fichier_existe(self, chemin_fichier): """ if not os.path.isfile(chemin_fichier): return False - return True \ No newline at end of file + return True + + def parse_fichier(self): + """ + Effectue une analyse syntaxique du fichier de log Apache puis retourne + une représentation du fichier avec les informations trouvées. + Returns: + log_analyse (FichierLogApache): Représentation du fichier. + Raises: + FormatLogApacheInvalideException: Format du fichier log invalide. + """ + log_analyse = FichierLogApache(self.chemin_log) + numero_ligne = 1 + with open(self.chemin_log, "r") as log: + for ligne in log: + try: + log_analyse.ajoute_entree(self.parse_entree(ligne)) + numero_ligne += 1 + except FormatLogApacheInvalideException as ex: + raise FormatLogApacheInvalideException( + f"Le format de l'entrée à la ligne {numero_ligne}" + f"('{ligne}') est invalide." + ) from ex + return log_analyse + + def parse_entree(self, entree): + """ + Effectue une analyse syntaxique d'une entrée dans un fichier de log + Apache puis retourne une représentation de l'entrée avec les + informations trouvées. + Args: + entree (str): Entrée à analyser. + Returns: + entree_analysee (EntreeLogApache): Représentation de l'entrée. + Raises: + FormatLogApacheInvalideException: Format de l'entrée du fichier log invalide. + """ + # Analyse de l'entrée + analyse = match(self.PATTERN_ENTREE_LOG_APACHE, entree) + if not analyse: + raise FormatLogApacheInvalideException() + resultat_analyse = analyse.groupdict() + # Récupération des informations liées au client + adresse_ip = self.get_information_entree(resultat_analyse, "ip") + identifiant_rfc = self.get_information_entree(resultat_analyse, "rfc") + utilisateur = self.get_information_entree(resultat_analyse, "utilisateur") + agent_utilisateur = self.get_information_entree(resultat_analyse, "agent_utilisateur") + informations_client = ClientInformations( + adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur + ) + # Récupération des informations liées à la requête + horodatage = self.get_information_entree(resultat_analyse, "horodatage") + if horodatage: + horodatage = datetime.strptime(horodatage, "%d/%b/%Y:%H:%M:%S %z") + methode_http = self.get_information_entree(resultat_analyse, "methode") + url = self.get_information_entree(resultat_analyse, "url") + protocole_http = self.get_information_entree(resultat_analyse, "protocole") + ancienne_url = self.get_information_entree(resultat_analyse, "ancienne_url") + informations_requete = RequeteInformations( + horodatage, methode_http, url, protocole_http, ancienne_url + ) + # Récupération des informations liées à la réponse + code_statut = self.get_information_entree(resultat_analyse, "code_status") + if code_statut: + code_statut = int(code_statut) + taille_octets = self.get_information_entree(resultat_analyse, "taille_octets") + if taille_octets: + taille_octets = int(taille_octets) + informations_reponse = ReponseInformations( + code_statut, taille_octets + ) + return EntreeLogApache( + informations_client, informations_requete, informations_reponse + ) + + def get_information_entree(self, analyse_regex, nom_information): + """ + Retourne la valeur de l'information dans l'analyse si elle possède une valeur + ou None si elle ne possède pas de valeur (égale à - ou vide). + Args: + analyse_regex (Match[str]): Résultat du regex de l'analyse. + nom_information (str): Nom de l'information souhaitée. + Returns: + Union[str, None]: La valeur sous forme de chaîne de caractère ou None si + aucune valeur n'a été trouvée. + """ + if nom_information in analyse_regex: + valeur = analyse_regex[nom_information] + if valeur != "-" and valeur != "": + return valeur + return None + + + + +class FormatLogApacheInvalideException(Exception): + + def __init__(self, *args): + super().__init__(*args) \ No newline at end of file From 9cbe38f4230f615cd08c372380298ad66edf63d9 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:23:46 +0100 Subject: [PATCH 23/47] =?UTF-8?q?Fix:=20Am=C3=A9lioration=20du=20regex=20p?= =?UTF-8?q?our=20les=20entr=C3=A9es=20des=20logs=20Apache=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Amélioration du regex en séparent les groupes ancienne_url et agent_utilisateur afin de permettre à une entrée d'avoir une ancienne url mais pas un agent utilisateur (ou inversement), tandis que ce n'était pas possible dans les versios précédentes --- app/parse/parseur_log_apache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py index 557d4ed..5606157 100644 --- a/app/parse/parseur_log_apache.py +++ b/app/parse/parseur_log_apache.py @@ -23,7 +23,7 @@ class ParseurLogApache(): r'(?P\S+) (?P\S+) (?P\S+)' r' (\[(?P.+?)\]|-) "((?P\S+) (?P\S+) (?P\S+)|-)"' r' (?P\d+) (?P\d+|-)' - r'( "(?P.*?)" "(?P.*?)")?' + r'( "(?P.*?)")?( "(?P.*?)")?' ) def __init__(self, chemin_log): From d54e9f7b9e32f9c74d9089ed3ac29cb94332cfb1 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:28:38 +0100 Subject: [PATCH 24/47] Chore: Modification .gitignore (#15) - Modification de l'erreur .coverage/ en .coverage puisque .coverage est un fichier et non un dossier --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e363721..3d74b68 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,5 @@ __pycache__/ .pytest_cache/ # Fichiers propres à Coverage -.coverage/ +.coverage htmlcov/ \ No newline at end of file From 75da8ed8d5cff9612e4a096f42193f0e8e186539 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:07:14 +0100 Subject: [PATCH 25/47] Test: Ajout des tests unitaires pour ParseurLogApache (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout des tests unitaires pour la classe ParseurLogApache afin de vérifier que la classe parvient à faire une analyse syntaxique correcte d'un fichier de log Apache --- tests/test_parseur_log_apache.py | 175 +++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 tests/test_parseur_log_apache.py diff --git a/tests/test_parseur_log_apache.py b/tests/test_parseur_log_apache.py new file mode 100644 index 0000000..7dd5c4e --- /dev/null +++ b/tests/test_parseur_log_apache.py @@ -0,0 +1,175 @@ +""" +Module des tests unitaires pour le parseur de fichier de log Apache. +""" + +import pytest +from re import match +from datetime import datetime, timezone, timedelta +from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException + +# Données utilisées pour les tests unitaires + +# Liste d'entrées valides +lignes_log_apache = [ + # Première entrée + '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 532', + # Deuxième entrée + '::1 - - [05/Mar/2025:16:59:43 +0100] "POST /backend/getConnexion.php HTTP/1.1" 200 20' + '"http://localhost/backend/connexion.php?titre=Connexion" "Mozilla/5.0 (Windows NT 10.0;' + ' Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"', + # Troisième entrée + '::1 - - [05/Mar/2025:16:59:43 +0100] "DELETE / HTTP/2.1" 200 20' +] + +# Liste d'entrées invalides +lignes_log_apache_invalides = [ + '', + 'Une ligne avec un format invalide !', + '::1 - - [05/Mar/2025:16:59:43] "DELETE / HTTP/2.1" 200 20', + '192.168.1.1 - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" test 532' +] + +@pytest.fixture() +def log_apache(tmp_path): + """ + Fixture pour créer et récupérer un fichier de log Apache temporaire. + Cette fixture permet de générer un fichier de log Apache temporaire contenant + soit des lignes valides, soit des lignes invalides selon le paramètre fourni. + Args: + tmp_path (Path): Chemin temporaire fourni par pytest. + Returns: + Callable[[bool], Path]: Une fonction qui crée et retourne le chemin + du fichier de log temporaire. + """ + def _creer_log(valide): + """ + Crée un fichier de log Apache temporaire. + Args: + valide (bool): Si True, le fichier contient des lignes de log valides. + Sinon, il contient des lignes invalides. + Returns: + Path: Le chemin du fichier de log temporaire créé. + """ + contenu = ( + "\n".join(lignes_log_apache) + if valide == True + else "\n".join(lignes_log_apache_invalides) + ) + fichier_temp = tmp_path / "access.log" + fichier_temp.write_text(contenu) + return fichier_temp + return _creer_log + +@pytest.fixture +def parseur_log_apache(log_apache, request): + """ + Fixture pour initialiser le parseur de log Apache. + Retourne une instance de la classe ParseurLogApache pour être utilisée dans les tests. + Returns: + ParseurLogApache: Une instance de la classe ParseurArgumentsCLI. + """ + if hasattr(request, "param") and request.param == False: + return ParseurLogApache(log_apache(False)) + return ParseurLogApache(log_apache(True)) + + +# Tests unitaires + +def test_exception_fichier_invalide(): + """ + Vérifie qu'une exception est bien levée lorsque le fichier n'existe pas. + Returns: + None + """ + with pytest.raises(FileNotFoundError): + parseur = ParseurLogApache("fichier/existe/pas.txt") + +@pytest.mark.parametrize("parseur_log_apache", [False], indirect=["parseur_log_apache"]) +def test_exception_fichier_invalide(parseur_log_apache): + """ + Vérifie qu'une exception est bien levée lorsque le format d'un fichier n'est + pas valide (une entrée invalide). + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurLogApache. + Returns: + None + """ + with pytest.raises(FormatLogApacheInvalideException): + fichier = parseur_log_apache.parse_fichier() + +@pytest.mark.parametrize("ligne_log", lignes_log_apache_invalides) +def test_exception_entree_invalide(parseur_log_apache, ligne_log): + """ + Vérifie qu'une exception est bien levée lorsque le format d'au moins une + entrée est invalide dans un fichier de log Apache. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurLogApache. + ligne_log (str): L'entrée à analyser. + Returns: + None + """ + with pytest.raises(FormatLogApacheInvalideException): + parseur_log_apache.parse_entree(ligne_log) + +def test_nombre_entrees_valide(parseur_log_apache): + """ + Vérifie que le nombre d'entrées trouvé correspond au nombre de ligne dans le log. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurLogApache. + Returns: + None + """ + fichier_log = parseur_log_apache.parse_fichier() + assert len(fichier_log.entrees) == len(lignes_log_apache) + +@pytest.mark.parametrize("nom_information, retour_attendu", [ + ("ip", "192.168.1.1"), + ("rfc", None), + ("horodatage", "12/Jan/2025:10:15:32 +0000"), + ("Existe pas !", None) +]) +def test_regex_recuperation_information_entree(parseur_log_apache, nom_information, retour_attendu): + """ + Vérifie que la récupération des informations à partir d'un résultat de regex fonctionne + correctement et que toutes valeurs introuvables ou égales à - renvoient None. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurLogApache. + nom_information (str): Nom de l'information à récupérer. + retour_attendu (Union[None, str]): La valeur attendue de l'information. + Returns: + None + """ + ligne = '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 532' + analyse = match(parseur_log_apache.PATTERN_ENTREE_LOG_APACHE, ligne) + resultat_analyse = analyse.groupdict() + assert parseur_log_apache.get_information_entree(resultat_analyse, nom_information) == retour_attendu + +def test_parsage_entree_valide(parseur_log_apache): + """ + Vérifie qu'une entrée est correctement analysée et que les informations partent + au bon endroit avec le bon typage. + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe ParseurLogApache. + Returns: + None + """ + ligne = '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" ' \ + '200 532 "/home" "Chrome/133.0.0.0"' + entree = parseur_log_apache.parse_entree(ligne) + assert entree.client.adresse_ip == "192.168.1.1" + assert entree.client.identifiant_rfc == None + assert entree.client.nom_utilisateur == None + assert entree.client.agent_utilisateur == "Chrome/133.0.0.0" + assert entree.requete.horodatage == datetime(2025, 1, 12, 10, 15, 32, + tzinfo=timezone(timedelta(hours=0))) + assert entree.requete.methode_http == "GET" + assert entree.requete.url == "/index.html" + assert entree.requete.protocole_http == "HTTP/1.1" + assert entree.requete.ancienne_url == "/home" + assert entree.reponse.code_status_http == 200 + assert entree.reponse.taille_octets == 532 From 4870023c307f37ad652137b42951361bddb73ea0 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:15:21 +0100 Subject: [PATCH 26/47] Fix: Correction du regex de log Apache pour l'horodatage (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modification du regex qui représente une entrée d'un fichier de log Apache afin qu'il vérifie également que la date dans une entrée est dans un format valide --- app/parse/parseur_log_apache.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py index 5606157..9f74025 100644 --- a/app/parse/parseur_log_apache.py +++ b/app/parse/parseur_log_apache.py @@ -21,7 +21,8 @@ class ParseurLogApache(): PATTERN_ENTREE_LOG_APACHE = ( r'(?P\S+) (?P\S+) (?P\S+)' - r' (\[(?P.+?)\]|-) "((?P\S+) (?P\S+) (?P\S+)|-)"' + r' (\[(?P\d{2}\/\w{3}\/\d{4}:\d{1,2}:\d{1,2}:\d{1,2} \+\d{4})\]|-)' + r' "((?P\S+) (?P\S+) (?P\S+)|-)"' r' (?P\d+) (?P\d+|-)' r'( "(?P.*?)")?( "(?P.*?)")?' ) @@ -139,9 +140,6 @@ def get_information_entree(self, analyse_regex, nom_information): return valeur return None - - - class FormatLogApacheInvalideException(Exception): def __init__(self, *args): From 1c5576b5687262f35c455530f9749d4664ea2686 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Wed, 26 Mar 2025 19:57:12 +0100 Subject: [PATCH 27/47] =?UTF-8?q?Feat:=20Am=C3=A9lioration=20des=20classes?= =?UTF-8?q?=20de=20donn=C3=A9es=20et=20du=20parseur=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Utilisation de la bibliothèque typing et dataclasses - Simplification de la classe en dataclass - Ajout de vérification des types des attributs - Modifiation du test unitaire test_parsage_entree_valide suite à une erreur due aux précédents commits qui ont modifié le nom de l'attribut code_statut_http - Ajout de commentaires et de sauts de ligne aux méthodes de ParseurLogApache afin de rendre le fichier plus lisible - Suppression de la méthode __fichier_existe dans ParseurLogApache qui était utilisée qu'une seule fois en déplacant son contenu directement dans le __init__ - Ajout d'un except pour les Exceptions qui affiche un message en indiquant qu'une erreur interne s'est produite --- app/donnees/client_informations.py | 49 +++++++++++++++++++----- app/donnees/reponse_informations.py | 35 ++++++++++++++--- app/donnees/requete_informations.py | 58 +++++++++++++++++++++++------ app/main.py | 2 + app/parse/parseur_log_apache.py | 52 +++++++++++++++----------- tests/test_parseur_log_apache.py | 2 +- 6 files changed, 147 insertions(+), 51 deletions(-) diff --git a/app/donnees/client_informations.py b/app/donnees/client_informations.py index 09a8c6c..5332852 100644 --- a/app/donnees/client_informations.py +++ b/app/donnees/client_informations.py @@ -2,16 +2,45 @@ Module relatif aux informations d'un client dans un fichier de log Apache. """ +from dataclasses import dataclass +from typing import Optional + +@dataclass class ClientInformations: + """ + Représente les informations d'un client à partir d'une entrée d'un log Apache. + + Cette classe regroupe les données extraites d'une entrée de log, + qui concernent le client ayant effectué la requête au serveur Apache. + + Attributes: + adresse_ip (str): L'adresse IP du client. + identifiant_rfc (Optional[str]): L'identifiant RFC du client. + Peut être None si non fournie. + nom_utilisateur (Optional[str]): Le nom de l'utilisateur authentifié. + Peut être None si non fournie. + agent_utilisateur (Optional[str]): L'agent utilisateur (User-Agent). + Peut être None si non fournie. + + Raises: + TypeError: Si les attributs ne sont pas de type `str` ou `None`. + """ + adresse_ip: str + identifiant_rfc: Optional[str] + nom_utilisateur: Optional[str] + agent_utilisateur: Optional[str] - def __init__(self, - adresse_ip, - identifiant_rfc, - nom_utilisateur, - agent_utilisateur - ): - self.adresse_ip = adresse_ip - self.identifiant_rfc = identifiant_rfc - self.nom_utilisateur = nom_utilisateur - self.agent_utilisateur = agent_utilisateur \ No newline at end of file + def __post_init__(self): + # Validation de l'adresse IP + if not isinstance(self.adresse_ip, str): + raise TypeError("L'adresse IP est obligatoire et doit être une chaîne de caractères.") + # Validation de l'identifiant RFC + if self.identifiant_rfc != None and not isinstance(self.identifiant_rfc, str): + raise TypeError("L'identifiant RFC doit être une chaîne de caractères ou None.") + # Validation du nom d'utilisateur + if self.nom_utilisateur != None and not isinstance(self.nom_utilisateur, str): + raise TypeError("Le nom d'utilisateur doit être une chaîne de caractères ou None.") + # Validation de l'agent utilisateur + if self.agent_utilisateur != None and not isinstance(self.agent_utilisateur, str): + raise TypeError("L'agent utilisateur doit être une chaîne de caractères ou None.") \ No newline at end of file diff --git a/app/donnees/reponse_informations.py b/app/donnees/reponse_informations.py index f50ed72..01d081a 100644 --- a/app/donnees/reponse_informations.py +++ b/app/donnees/reponse_informations.py @@ -2,12 +2,35 @@ Module relatif aux informations de la réponse dans un fichier de log Apache. """ +from dataclasses import dataclass +from typing import Optional + +@dataclass class ReponseInformations: + """ + Représente les informations de la réponse HTTP à partir d'une entrée d'un log Apache. + + Cette classe regroupe les données extraites d'une entrée de log, + qui concernent les informations techniques sur la réponse émise par + le serveur Apache au client. + + Attributes: + code_statut_http (int): Le code de statut HTTP. + taille_octets (Optional[int]): La taille de la réponse en octets. + Peut être None si non fournie. + + Raises: + TypeError: Si les attributs ne sont pas du type int. + """ + + code_statut_http: int + taille_octets: Optional[int] - def __init__(self, - code_status_http, - taille_octets - ): - self.code_status_http = code_status_http - self.taille_octets = taille_octets + def __post_init__(self): + # Vérification du code de statut HTTP + if not isinstance(self.code_statut_http, int): + raise TypeError("Le code de statut HTTP doit être un entier.") + # Vérification de la taille de la réponse (en octets) + if self.taille_octets != None and not isinstance(self.taille_octets, int): + raise TypeError("La taille en octets doit être un entier ou None.") diff --git a/app/donnees/requete_informations.py b/app/donnees/requete_informations.py index 13fb74f..fc9c67d 100644 --- a/app/donnees/requete_informations.py +++ b/app/donnees/requete_informations.py @@ -2,18 +2,52 @@ Module relatif aux informations de la requete dans un fichier de log Apache. """ +from dataclasses import dataclass +from typing import Optional +from datetime import datetime +@dataclass class RequeteInformations: + """ + Représente les informations de la requête HTTP à partir d'une entrée d'un log Apache. - def __init__(self, - horodatage, - methode_http, - url, - protocole_http, - ancienne_url - ): - self.horodatage = horodatage - self.methode_http = methode_http - self.url = url - self.protocole_http = protocole_http - self.ancienne_url = ancienne_url \ No newline at end of file + Cette classe regroupe les données extraites d'une entrée de log, + qui concernent les informations techniques sur la requête émise au + serveur Apache. + + Attributes: + horodatage (datetime): L'horodatage de la requête. + methode_http (Optional[str]): La méthode HTTP utilisée. + Peut être None si non fournie. + url (Optional[str]): L'URL cible de la requête. + Peut être None si non fournie. + protocole_http (Optional[str]): Le protocole HTTP utilisé. + Peut être None si non fournie. + ancienne_url (Optional[str]): L'URL de provenance (referrer). + Peut être None si non fournie. + + Raises: + TypeError: Si les attributs ne sont pas du type attendu ou None. + """ + horodatage: datetime + methode_http: Optional[str] + url: Optional[str] + protocole_http: Optional[str] + ancienne_url: Optional[str] + + def __post_init__(self): + # Vérification de l'horodatage + if not isinstance(self.horodatage, datetime): + raise TypeError("L'horodatage doit être de type datetime.") + # Vérification de la méthode HTTP + if self.methode_http != None and not isinstance(self.methode_http, str): + raise TypeError("La méthode HTTP doit être une chaine de caractère ou None.") + # Vérification de la ressource demandée + if self.url != None and not isinstance(self.url, str): + raise TypeError("L'URL doit être une chaine de caractère ou None.") + # Vérification du protocole HTTP + if self.protocole_http != None and not isinstance(self.protocole_http, str): + raise TypeError("Le protocole HTTP doit être une chaine de caractère ou None.") + # Vérification de l'ancienne URL + if self.ancienne_url != None and not isinstance(self.ancienne_url, str): + raise TypeError("L'ancienne URL doit être une chaine de caractère ou None.") \ No newline at end of file diff --git a/app/main.py b/app/main.py index f0467cd..7c7b6ce 100644 --- a/app/main.py +++ b/app/main.py @@ -36,3 +36,5 @@ print(f"Erreur dans la recherche du log Apache !\n{ex}") except FormatLogApacheInvalideException as ex: print(f"Erreur dans l'analyse du log Apache !\n{ex}") + except Exception as ex: + print(f"Erreur interne !\n{ex}") diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py index 9f74025..a439936 100644 --- a/app/parse/parseur_log_apache.py +++ b/app/parse/parseur_log_apache.py @@ -36,19 +36,9 @@ def __init__(self, chemin_log): Raises: FileNotFoundError: Si le fichier à analyser est introuvable. """ - if not self.__fichier_existe(chemin_log): + if not os.path.isfile(chemin_log): raise FileNotFoundError(f"Le fichier {chemin_log} est introuvable.") self.chemin_log = chemin_log - - def __fichier_existe(self, chemin_fichier): - """ - Vérifie que le chemin passé en paramètre correspond à une fichier existant. - Returns: - bool: True s'il existe, False sinon. - """ - if not os.path.isfile(chemin_fichier): - return False - return True def parse_fichier(self): """ @@ -60,17 +50,17 @@ def parse_fichier(self): FormatLogApacheInvalideException: Format du fichier log invalide. """ log_analyse = FichierLogApache(self.chemin_log) - numero_ligne = 1 with open(self.chemin_log, "r") as log: - for ligne in log: + for numero_ligne, ligne in enumerate(log, start=1): try: - log_analyse.ajoute_entree(self.parse_entree(ligne)) - numero_ligne += 1 + entree = self.parse_entree(ligne) + log_analyse.ajoute_entree(entree) except FormatLogApacheInvalideException as ex: raise FormatLogApacheInvalideException( - f"Le format de l'entrée à la ligne {numero_ligne}" - f"('{ligne}') est invalide." + f"Le format de l'entrée à la ligne {numero_ligne} " + f"('{ligne.strip()}') est invalide." ) from ex + return log_analyse def parse_entree(self, entree): @@ -89,40 +79,61 @@ def parse_entree(self, entree): analyse = match(self.PATTERN_ENTREE_LOG_APACHE, entree) if not analyse: raise FormatLogApacheInvalideException() + + # Extraction des résultats d'analyse resultat_analyse = analyse.groupdict() + # Récupération des informations liées au client adresse_ip = self.get_information_entree(resultat_analyse, "ip") + if adresse_ip is None: + raise FormatLogApacheInvalideException("L'adresse IP est obligatoire.") identifiant_rfc = self.get_information_entree(resultat_analyse, "rfc") utilisateur = self.get_information_entree(resultat_analyse, "utilisateur") agent_utilisateur = self.get_information_entree(resultat_analyse, "agent_utilisateur") + informations_client = ClientInformations( adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur ) + # Récupération des informations liées à la requête horodatage = self.get_information_entree(resultat_analyse, "horodatage") if horodatage: horodatage = datetime.strptime(horodatage, "%d/%b/%Y:%H:%M:%S %z") + + if horodatage is None: + raise FormatLogApacheInvalideException("L'horodatage est obligatoire.") + methode_http = self.get_information_entree(resultat_analyse, "methode") url = self.get_information_entree(resultat_analyse, "url") protocole_http = self.get_information_entree(resultat_analyse, "protocole") ancienne_url = self.get_information_entree(resultat_analyse, "ancienne_url") + informations_requete = RequeteInformations( horodatage, methode_http, url, protocole_http, ancienne_url ) + # Récupération des informations liées à la réponse code_statut = self.get_information_entree(resultat_analyse, "code_status") if code_statut: code_statut = int(code_statut) + + if code_statut is None: + raise FormatLogApacheInvalideException("Le code de statut HTTP est obligatoire.") + taille_octets = self.get_information_entree(resultat_analyse, "taille_octets") if taille_octets: taille_octets = int(taille_octets) + informations_reponse = ReponseInformations( code_statut, taille_octets ) + + # Retour des informations regroupées dans l'objet EntreeLogApache return EntreeLogApache( informations_client, informations_requete, informations_reponse ) + def get_information_entree(self, analyse_regex, nom_information): """ Retourne la valeur de l'information dans l'analyse si elle possède une valeur @@ -134,11 +145,8 @@ def get_information_entree(self, analyse_regex, nom_information): Union[str, None]: La valeur sous forme de chaîne de caractère ou None si aucune valeur n'a été trouvée. """ - if nom_information in analyse_regex: - valeur = analyse_regex[nom_information] - if valeur != "-" and valeur != "": - return valeur - return None + valeur = analyse_regex.get(nom_information) + return valeur if valeur != "" and valeur != "-" else None class FormatLogApacheInvalideException(Exception): diff --git a/tests/test_parseur_log_apache.py b/tests/test_parseur_log_apache.py index 7dd5c4e..d217f6f 100644 --- a/tests/test_parseur_log_apache.py +++ b/tests/test_parseur_log_apache.py @@ -171,5 +171,5 @@ def test_parsage_entree_valide(parseur_log_apache): assert entree.requete.url == "/index.html" assert entree.requete.protocole_http == "HTTP/1.1" assert entree.requete.ancienne_url == "/home" - assert entree.reponse.code_status_http == 200 + assert entree.reponse.code_statut_http == 200 assert entree.reponse.taille_octets == 532 From b11265dee102d764fd930f6ecb214c7a9759b354 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:30:52 +0100 Subject: [PATCH 28/47] Docs: Initialisation de la documentation du projet (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Utilisation de la bibliothèque Sphinx - Ajout du dossier 'docs' contenant toute la documentation du projet - Ajout des fichiers .rst pour ajouter une page d'accueil ainsi que les modules dans la documentation - Modification des docstrings des classes dans le dossier donnees ( modification mot-clé Attributes en Args) afin d'éviter des erreurs de génération avec Sphinx --- .gitignore | 5 ++- app/donnees/client_informations.py | 2 +- app/donnees/reponse_informations.py | 2 +- app/donnees/requete_informations.py | 2 +- docs/Makefile | 20 ++++++++++ docs/make.bat | 35 +++++++++++++++++ docs/source/_static/documentation.css | 4 ++ docs/source/conf.py | 39 +++++++++++++++++++ docs/source/index.rst | 22 +++++++++++ docs/source/modules/cli/index_cli.rst | 7 ++++ .../modules/cli/parseur_arguments_cli.rst | 7 ++++ .../modules/donnees/client_informations.rst | 7 ++++ docs/source/modules/donnees/index_donnees.rst | 9 +++++ .../modules/donnees/reponse_informations.rst | 7 ++++ .../modules/donnees/requete_informations.rst | 8 ++++ docs/source/modules/index_modules.rst | 11 ++++++ docs/source/modules/main.rst | 7 ++++ .../modules/parse/fichier_log_apache.rst | 7 ++++ docs/source/modules/parse/index_parse.rst | 8 ++++ .../modules/parse/parseur_log_apache.rst | 7 ++++ 20 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_static/documentation.css create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/modules/cli/index_cli.rst create mode 100644 docs/source/modules/cli/parseur_arguments_cli.rst create mode 100644 docs/source/modules/donnees/client_informations.rst create mode 100644 docs/source/modules/donnees/index_donnees.rst create mode 100644 docs/source/modules/donnees/reponse_informations.rst create mode 100644 docs/source/modules/donnees/requete_informations.rst create mode 100644 docs/source/modules/index_modules.rst create mode 100644 docs/source/modules/main.rst create mode 100644 docs/source/modules/parse/fichier_log_apache.rst create mode 100644 docs/source/modules/parse/index_parse.rst create mode 100644 docs/source/modules/parse/parseur_log_apache.rst diff --git a/.gitignore b/.gitignore index 3d74b68..dc69735 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ __pycache__/ # Fichiers propres à Coverage .coverage -htmlcov/ \ No newline at end of file +htmlcov/ + +# Fichiers de la documentation sphinx +docs/build/ \ No newline at end of file diff --git a/app/donnees/client_informations.py b/app/donnees/client_informations.py index 5332852..b524f02 100644 --- a/app/donnees/client_informations.py +++ b/app/donnees/client_informations.py @@ -14,7 +14,7 @@ class ClientInformations: Cette classe regroupe les données extraites d'une entrée de log, qui concernent le client ayant effectué la requête au serveur Apache. - Attributes: + Args: adresse_ip (str): L'adresse IP du client. identifiant_rfc (Optional[str]): L'identifiant RFC du client. Peut être None si non fournie. diff --git a/app/donnees/reponse_informations.py b/app/donnees/reponse_informations.py index 01d081a..0e702e2 100644 --- a/app/donnees/reponse_informations.py +++ b/app/donnees/reponse_informations.py @@ -15,7 +15,7 @@ class ReponseInformations: qui concernent les informations techniques sur la réponse émise par le serveur Apache au client. - Attributes: + Args: code_statut_http (int): Le code de statut HTTP. taille_octets (Optional[int]): La taille de la réponse en octets. Peut être None si non fournie. diff --git a/app/donnees/requete_informations.py b/app/donnees/requete_informations.py index fc9c67d..5e4b47b 100644 --- a/app/donnees/requete_informations.py +++ b/app/donnees/requete_informations.py @@ -15,7 +15,7 @@ class RequeteInformations: qui concernent les informations techniques sur la requête émise au serveur Apache. - Attributes: + Args: horodatage (datetime): L'horodatage de la requête. methode_http (Optional[str]): La méthode HTTP utilisée. Peut être None si non fournie. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/documentation.css b/docs/source/_static/documentation.css new file mode 100644 index 0000000..81f0a2f --- /dev/null +++ b/docs/source/_static/documentation.css @@ -0,0 +1,4 @@ +/* Étendre le contenu principal sur toute la largeur */ +.wy-nav-content { + max-width: 100%; +} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..507551a --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,39 @@ +import os +import sys + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'LogBuster' +copyright = '2025, Anthony GUILLAUMA' +author = 'Anthony GUILLAUMA' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon'] + +templates_path = ['_templates'] +exclude_patterns = [] + +language = 'fr' + +# Ajout du chemin vers les modules +sys.path.insert(0, os.path.abspath('../../app')) + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_css_files = ["documentation.css"] +html_theme_options = { + "collapse_navigation": False, # Pour éviter la fermeture automatique + "sticky_navigation": True, # Pour rendre le menu toujours visible + "titles_only": False # Pour éviter de masquer les sous-sections +} \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..d21ae8e --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,22 @@ +.. Documentation de LogBuster + +Bienvenue dans la documentation de LogBuster +============================================ + +LogBuster est un outil puissant pour analyser les fichiers de log Apache. +Il vous permet de : + +- Extraire des statistiques clés. +- Exporter les données en JSON. +- Gérer les erreurs de format avec précision. + +.. toctree:: + :maxdepth: 4 + :caption: Contenu + :numbered: + + modules/index_modules.rst + +--- + +© 2025 - Projet LogBuster diff --git a/docs/source/modules/cli/index_cli.rst b/docs/source/modules/cli/index_cli.rst new file mode 100644 index 0000000..5494b23 --- /dev/null +++ b/docs/source/modules/cli/index_cli.rst @@ -0,0 +1,7 @@ +CLI +=========== + +.. toctree:: + :maxdepth: 4 + + parseur_arguments_cli.rst diff --git a/docs/source/modules/cli/parseur_arguments_cli.rst b/docs/source/modules/cli/parseur_arguments_cli.rst new file mode 100644 index 0000000..2ffeeef --- /dev/null +++ b/docs/source/modules/cli/parseur_arguments_cli.rst @@ -0,0 +1,7 @@ +ParseurArgumentsCLI +====================== + +.. automodule:: cli.parseur_arguments_cli + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/donnees/client_informations.rst b/docs/source/modules/donnees/client_informations.rst new file mode 100644 index 0000000..73a48e5 --- /dev/null +++ b/docs/source/modules/donnees/client_informations.rst @@ -0,0 +1,7 @@ +ClientInformations +====================== + +.. automodule:: donnees.client_informations + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/donnees/index_donnees.rst b/docs/source/modules/donnees/index_donnees.rst new file mode 100644 index 0000000..b863f8b --- /dev/null +++ b/docs/source/modules/donnees/index_donnees.rst @@ -0,0 +1,9 @@ +Données +=========== + +.. toctree:: + :maxdepth: 4 + + client_informations.rst + requete_informations.rst + reponse_informations.rst \ No newline at end of file diff --git a/docs/source/modules/donnees/reponse_informations.rst b/docs/source/modules/donnees/reponse_informations.rst new file mode 100644 index 0000000..e45f565 --- /dev/null +++ b/docs/source/modules/donnees/reponse_informations.rst @@ -0,0 +1,7 @@ +ReponseInformations +====================== + +.. automodule:: donnees.reponse_informations + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/donnees/requete_informations.rst b/docs/source/modules/donnees/requete_informations.rst new file mode 100644 index 0000000..4662f22 --- /dev/null +++ b/docs/source/modules/donnees/requete_informations.rst @@ -0,0 +1,8 @@ +RequeteInformations +====================== + +.. automodule:: donnees.requete_informations + :members: + :show-inheritance: + :undoc-members: + :exclude-members: dataclass diff --git a/docs/source/modules/index_modules.rst b/docs/source/modules/index_modules.rst new file mode 100644 index 0000000..1a85a35 --- /dev/null +++ b/docs/source/modules/index_modules.rst @@ -0,0 +1,11 @@ +Modules +======================== + +.. toctree:: + :maxdepth: 4 + :caption: Modules: + + main.rst + cli/index_cli.rst + parse/index_parse.rst + donnees/index_donnees.rst \ No newline at end of file diff --git a/docs/source/modules/main.rst b/docs/source/modules/main.rst new file mode 100644 index 0000000..94b7718 --- /dev/null +++ b/docs/source/modules/main.rst @@ -0,0 +1,7 @@ +Main +====================== + +.. automodule:: main + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/parse/fichier_log_apache.rst b/docs/source/modules/parse/fichier_log_apache.rst new file mode 100644 index 0000000..e758d38 --- /dev/null +++ b/docs/source/modules/parse/fichier_log_apache.rst @@ -0,0 +1,7 @@ +FichierLogApache +====================== + +.. automodule:: parse.fichier_log_apache + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/parse/index_parse.rst b/docs/source/modules/parse/index_parse.rst new file mode 100644 index 0000000..6fe508c --- /dev/null +++ b/docs/source/modules/parse/index_parse.rst @@ -0,0 +1,8 @@ +Parse +=========== + +.. toctree:: + :maxdepth: 4 + + parseur_log_apache.rst + fichier_log_apache.rst \ No newline at end of file diff --git a/docs/source/modules/parse/parseur_log_apache.rst b/docs/source/modules/parse/parseur_log_apache.rst new file mode 100644 index 0000000..173d73f --- /dev/null +++ b/docs/source/modules/parse/parseur_log_apache.rst @@ -0,0 +1,7 @@ +ParseurLogApache +====================== + +.. automodule:: parse.parseur_log_apache + :members: + :show-inheritance: + :undoc-members: From 86067d658644ce3a60f068ee7fcd7b46cd694e81 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:24:39 +0100 Subject: [PATCH 29/47] CI: Documentation (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout du dossier .github et workflows - Ajout du fichier documentation.yaml qui contient l'action pour générer la documentation - Ajout de l'action sur chaque push vers la branche develop --- .github/workflows/documentation.yaml | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/documentation.yaml diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml new file mode 100644 index 0000000..0b22f51 --- /dev/null +++ b/.github/workflows/documentation.yaml @@ -0,0 +1,51 @@ +name: Construction et déploiement de la configuration + +on: + push: + branches: + - develop + +permissions: + contents: read + pages: write + id-token: write # Nécessaire pour déployer sur GitHub Pages + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Positionnement sur le dépôt + uses: actions/checkout@v4 + + - name: Mis en place de Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Installation des dépendances + run: | + python -m pip install --upgrade pip + pip install sphinx + pip install sphinx_rtd_theme --break-system-packages + + - name: Construction de la documentation (avec Sphinx) + run: | + sphinx-build -b html docs/source docs/build/html + + - name: Publication la documentation générée + uses: actions/upload-pages-artifact@v3 + with: + path: docs/build/html # Dossier contenant la doc générée + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file From 29b6996be21f0af9e07b4b614da0ec07ce51e301 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:44:52 +0100 Subject: [PATCH 30/47] CI: Ajout des tests unitaires automatiques (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout des tests unitaires automatiques incluant un test de couverture - Ajout des tests sur les push de la branche develop - Les tests seront effectuées sur plusieurs versions de Python - Création d'un artéfact pour chaque version Python - Les artefacts contiennent les rapports de tests et de couverture --- .github/workflows/tests.yaml | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..cab9cb8 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,49 @@ +name: Tests unitaires - LogBuster + +on: + push: + branches: + - develop + +# Permissions (lecture uniquement) +permissions: + contents: read + +jobs: + pytest: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] # Liste des versions de Python à tester + + steps: + # Étape 1 : Cloner le dépôt + - name: Cloner le dépôt + uses: actions/checkout@v4 + + # Étape 2 : Installer Python + - name: Installer Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + # Étape 3 : Installer les dépendances + - name: Installer les dépendances + run: | + python -m pip install --upgrade pip + pip install pytest + pip install pytest-cov + + # Étape 4 : Lancer les tests unitaires + - name: Lancer les tests unitaires + run: | + 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 + + # Étape 5 : Sauvegarder les artefacts + - name: Sauvegarder les résultats de test + if: always() # Sauvegarde même si les tests échouent + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: tests-resultats-python-${{ matrix.python-version }} # Nom de l'artefact + path: resultats_pytest # Eléments à sauvegarder From c30d2f8507abf563a2830497f165ee14b2231b18 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:09:05 +0100 Subject: [PATCH 31/47] =?UTF-8?q?Fix:=20Interdiction=20des=20bool=C3=A9ens?= =?UTF-8?q?=20dans=20la=20classe=20ReponseInformations=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout d'une vérification pour interdire les booléens dans les attributs de la classe ReponseInformations --- app/donnees/reponse_informations.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/donnees/reponse_informations.py b/app/donnees/reponse_informations.py index 0e702e2..8fcd125 100644 --- a/app/donnees/reponse_informations.py +++ b/app/donnees/reponse_informations.py @@ -29,8 +29,11 @@ class ReponseInformations: def __post_init__(self): # Vérification du code de statut HTTP - if not isinstance(self.code_statut_http, int): + if (not isinstance(self.code_statut_http, int) + or isinstance(self.code_statut_http, bool)): raise TypeError("Le code de statut HTTP doit être un entier.") # Vérification de la taille de la réponse (en octets) - if self.taille_octets != None and not isinstance(self.taille_octets, int): + if (self.taille_octets != None + and not isinstance(self.taille_octets, int) + or isinstance(self.taille_octets, bool)): raise TypeError("La taille en octets doit être un entier ou None.") From 614520a55774bc656f950a8481ab16d2f69c939d Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:15:08 +0100 Subject: [PATCH 32/47] =?UTF-8?q?Test:=20Ajout=20des=20tests=20unitaires?= =?UTF-8?q?=20pour=20les=20classes=20des=20donn=C3=A9es=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout des tests unitaires pour les classes ClientInformations, RequeteInformations et ReponseInformations --- tests/test_donnees_log_apache.py | 117 +++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/test_donnees_log_apache.py diff --git a/tests/test_donnees_log_apache.py b/tests/test_donnees_log_apache.py new file mode 100644 index 0000000..81778cd --- /dev/null +++ b/tests/test_donnees_log_apache.py @@ -0,0 +1,117 @@ +""" +Module des tests unitaires pour les classes contenant les données des logs Apache +(ClientInformations, RequetesInformations et ReponseInformations). +""" + +import pytest +from datetime import datetime, timezone, timedelta +from donnees.client_informations import ClientInformations +from donnees.requete_informations import RequeteInformations +from donnees.reponse_informations import ReponseInformations + +@pytest.mark.parametrize("adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur", [ + ("192.168.0.1", "rfc", "utilisateur", "Mozilla/5.0") +]) +def test_client_informations_valide(adresse_ip, + identifiant_rfc, + utilisateur, + agent_utilisateur): + client = ClientInformations( + adresse_ip, + identifiant_rfc, + utilisateur, + agent_utilisateur + ) + assert client.adresse_ip == adresse_ip + assert client.identifiant_rfc == identifiant_rfc + assert client.nom_utilisateur == utilisateur + assert client.agent_utilisateur == agent_utilisateur + +@pytest.mark.parametrize("adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur", [ + (False, "rfc", "utilisateur", "Mozilla/5.0"), + ("192.168.0.1", False, "utilisateur", "Mozilla/5.0"), + ("192.168.0.1", "rfc", False, "Mozilla/5.0"), + ("192.168.0.1", "rfc", "utilisateur", False) +]) +def test_client_exception_type_invalide(adresse_ip, + identifiant_rfc, + utilisateur, + agent_utilisateur): + with pytest.raises(TypeError): + client = ClientInformations( + adresse_ip, + identifiant_rfc, + utilisateur, + agent_utilisateur + ) + +@pytest.mark.parametrize("horodatage, methode_http, url, protocole_http, ancienne_url", [ + (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), + "POST", "essaie.fr/contact", "HTTP/1.2", "essaie.fr/accueil") +]) +def test_requete_informations_valide(horodatage, + methode_http, + url, + protocole_http, + ancienne_url): + requete = RequeteInformations( + horodatage, + methode_http, + url, + protocole_http, + ancienne_url + ) + assert requete.horodatage == horodatage + assert requete.methode_http == methode_http + assert requete.url == url + assert requete.protocole_http == protocole_http + assert requete.ancienne_url == ancienne_url + +@pytest.mark.parametrize("horodatage, methode_http, url, protocole_http, ancienne_url", [ + (False, "POST", "essaie.fr/contact", "HTTP/1.2", "essaie.fr/accueil"), + (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), + False, "essaie.fr/contact", "HTTP/1.2", "essaie.fr/accueil"), + (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), + "POST", False, "HTTP/1.2", "essaie.fr/accueil"), + (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), + "POST", "essaie.fr/contact", False, "essaie.fr/accueil"), + (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), + "POST", "essaie.fr/contact", "HTTP/1.2", False), +]) +def test_requete_exception_type_invalide(horodatage, + methode_http, + url, + protocole_http, + ancienne_url): + with pytest.raises(TypeError): + requete = RequeteInformations( + horodatage, + methode_http, + url, + protocole_http, + ancienne_url + ) + +@pytest.mark.parametrize("code_statut_http, taille_octets", [ + (404, 50) +]) +def test_reponse_informations_valide(code_statut_http, + taille_octets): + reponse = ReponseInformations( + code_statut_http, + taille_octets + ) + assert reponse.code_statut_http == code_statut_http + assert reponse.taille_octets == taille_octets + +@pytest.mark.parametrize("code_statut_http, taille_octets", [ + (False, 50), + (404, False) +]) +def test_reponse_exception_type_invalide(code_statut_http, + taille_octets): + with pytest.raises(TypeError): + reponse = ReponseInformations( + code_statut_http, + taille_octets + ) \ No newline at end of file From 8aa62d80e5a2b2d7918286f6bdecb32e9ed17a88 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 29 Mar 2025 08:57:34 +0100 Subject: [PATCH 33/47] Feat: Ajout de l'analyse statistique de fichier de log Apache (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de la classe AnalyseurLogApache qui retourne des statistiques sur un fichier de log Apache - Modification de main.py pour récupérer l'analyse complète du fichier --- app/analyse/analyseur_log_apache.py | 137 ++++++++++++++++++++++++++++ app/main.py | 5 +- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 app/analyse/analyseur_log_apache.py diff --git a/app/analyse/analyseur_log_apache.py b/app/analyse/analyseur_log_apache.py new file mode 100644 index 0000000..ee5c2b4 --- /dev/null +++ b/app/analyse/analyseur_log_apache.py @@ -0,0 +1,137 @@ +""" +Module pour l'analyse statistique d'un fichier log Apache. +""" + +from collections import Counter +from parse.fichier_log_apache import FichierLogApache + + +class AnalyseurLogApache: + """ + Représente un analysateur pour faire une analyse statistique d'un fichier + log Apache et créer des statistiques à partir de ce dernier. + Attributes: + fichier (FichierLogApache): Le fichier de log Apache à analyser. + nombre_par_top (int): Le nombre maximal d'éléments à inclure dans + les statistiques des classements (tops). + """ + + def __init__(self, fichier_log_apache: FichierLogApache, nombre_par_top: int = 3): + """ + Initialise un nouveau analysateur de fichier log Apache. + Args: + fichier_log_apache (FichierLogApache): Le fichier à analyser. + nombre_par_top (int): Le nombre maximal d'éléments à inclure dans + les statistiques des classements (tops). Par défaut, sa valeur est égale à ``3``. + Raises: + TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance de :class:`FichierLogApache` + ou si l'argument ``nombre_par_top`` n'est pas un entier. + ValueError: Si l'argument ``nombre_par_top`` est inférieur à ``0``. + """ + if not isinstance(fichier_log_apache, FichierLogApache): + raise TypeError("La représentation du fichier doit être de type FichierLogApache.") + if not isinstance(nombre_par_top, int) or isinstance(nombre_par_top, bool): + raise TypeError("Le nombre par top doit être un entier.") + if nombre_par_top < 0: + raise ValueError("Le nombre par top doit être supérieur ou égale à 0.") + self.fichier = fichier_log_apache + self.nombre_par_top = nombre_par_top + + def _get_repartition_elements(self, + liste_elements: list, + nom_elements: str, + mode_top_classement: bool = False) -> list: + """ + Retourne le top 'n' des éléments qui apparaissent le plus dans la liste. + Args: + liste_elements (list): La liste des éléments. + nom_elements (str): Le nom des éléments. + mode_top_classement (bool): Indique si la méthode doit retourner ou non le top + 'n' des éléments les plus présents, où 'n' est égale à l'attribut + :attr:`nombre_par_top`. Par défaut, ce mode est désactivé (valeur à ``False``). + Returns: + list: Une liste de dictionnaires contenant, pour chaque élément : + - Sa valeur. + - Son nombre total d'apparitions. + - Son taux d'apparition dans la liste. + La liste est triée dans l'ordre décroissant selon le nombre d'apparitions. + Si ``mode_top_classement`` est défini à ``True``, elle contient au maximum + :attr:`nombre_par_top` éléments, mais peut en contenir moins s'il y a moins + de :attr:`nombre_par_top` éléments distincts. + """ + if not isinstance(liste_elements, list): + raise TypeError("La liste des éléments doit être une instance du type list.") + if not isinstance(nom_elements, str): + raise TypeError("Le nom des éléments doit être une chaîne de caractères") + if not isinstance(mode_top_classement, bool): + raise TypeError("L'indication de l'activation du mode 'top_classement' doi être un booléen.") + + total_elements = len(liste_elements) + compteur_elements = Counter(liste_elements) + top_elements = compteur_elements.most_common(self.nombre_par_top if mode_top_classement else None) + return [ + {nom_elements: element, "total": total, "taux": total / total_elements * 100} + for element, total in top_elements + ] + + def get_analyse_complete(self) -> dict: + """ + Retourne l'analyse complète du fichier de log Apache. + Returns: + dict: L'analyse sous forme d'un dictionnaire. + Le dictionnaire suit la structure suivante: + - chemin: chemin du fichier + - statistiques: + - requetes: + - top_urls: voir :meth:`get_top_urls` + - repartition_code_statut_http: voir :meth:`get_total_par_code_statut_http` + """ + return { + "chemin": self.fichier.chemin, + "statistiques": { + "total_entrees": self.get_total_entrees(), + "requetes": { + "top_urls": self.get_top_urls(), + "repartition_code_statut_http": self.get_total_par_code_statut_http() + } + } + } + + def get_total_entrees(self) -> int: + """ + Retourne le nombre total d'entrées dans le fichier. + Returns: + int: Le nombre total d'entrées. + """ + return len(self.fichier.entrees) + + def get_top_urls(self) -> list: + """ + Retourne le top :attr:`nombre_par_top` des urls les plus demandées. + Returns: + list: Une liste de dictionnaires où chaque clé contient : + - url: L'URL demandée. + - total: Le nombre total de fois où cette URL a été demandée. + - taux: Le pourcentage de demandes correspondant à cette URL. + La liste est triée dans l'ordre décroissant du nombre total d'apparitions. + """ + return self._get_repartition_elements( + [entree.requete.url for entree in self.fichier.entrees], + "url", + True + ) + + def get_total_par_code_statut_http(self) -> list: + """ + Retourne la répartition des réponses par code de statut htpp retourné. + Returns: + list: Une liste de dictionnaires où chaque clé contient : + - code: Le code de statut http. + - total: Le nombre total de fois où ce code a été demandée. + - taux: Le pourcentage de demandes correspondant à ce code. + La liste est triée dans l'ordre décroissant du nombre total d'apparitions. + """ + return self._get_repartition_elements( + [entree.reponse.code_statut_http for entree in self.fichier.entrees], + "code" + ) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 7c7b6ce..a1b7aaf 100644 --- a/app/main.py +++ b/app/main.py @@ -5,6 +5,7 @@ import colorama from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException +from analyse.analyseur_log_apache import AnalyseurLogApache if __name__ == "__main__": @@ -27,8 +28,10 @@ arguments_cli = parseur_cli.parse_args() # Analyse syntaxique du fichier log parseur_log = ParseurLogApache(arguments_cli.chemin_log) - parseur_log.parse_fichier() + fichier_log = parseur_log.parse_fichier() # Analyse statistique du fichier log + analyseur_log = AnalyseurLogApache(fichier_log) + analyse = analyseur_log.get_analyse_complete() # Exportation de l'analyse except ArgumentCLIException as ex: print(f"Erreur dans les arguments fournis !\n {ex}") From a974b23683dc33318f300ac2f28f671885473596 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 29 Mar 2025 14:52:25 +0100 Subject: [PATCH 34/47] Test: Ajout des tests unitaires pour AnalyseurLogApache (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création d'un fichier conftest.py qui contient toutes les fixtures et les données générales - Ajout des tests unitaires pour la classe AnalyseurLogApache --- tests/conftest.py | 123 +++++++++++++++++ tests/test_analyseur_log_apache.py | 204 ++++++++++++++++++++++++++++ tests/test_parseur_arguments_cli.py | 13 +- tests/test_parseur_log_apache.py | 74 +--------- 4 files changed, 334 insertions(+), 80 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_analyseur_log_apache.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bf31bf0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,123 @@ +""" +Module de configuration des tests unitaires. +""" + +import pytest +from cli.parseur_arguments_cli import ParseurArgumentsCLI +from parse.fichier_log_apache import FichierLogApache +from parse.parseur_log_apache import ParseurLogApache +from analyse.analyseur_log_apache import AnalyseurLogApache + + +# ----------------- +# Données générales +# ----------------- + +# Lignes respectant la syntaxe d'un fichier de log Apache +lignes_valides = [ + '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 532', + + '::1 - - [05/Mar/2025:16:59:43 +0100] "POST / HTTP/1.1" 500 20' + '"http://localhost/connexion.php" "Mozilla/5.0 (Windows NT 10.0;' + ' Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"', + + '::1 - - [05/Mar/2025:16:59:43 +0100] "DELETE /index.html HTTP/2.1" 500 20', + + '::1 - test [05/Mar/2025:16:59:59 +0100] "DELETE /index.html HTTP/2.1" 500 20', + + '111.89.7.3 - essaie [27/Feb/2025:10:0:0 +0110] "GET / HTTP/2.1" 500 20' +] + +# Lignes ne respectant pas la syntaxe d'un fichier de log Apache +lignes_invalides = [ + '', + + 'Une ligne avec un format invalide !', + + '::1 - - [05/Mar/2025:16:59:43] "DELETE / HTTP/2.1" 200 20', + + '192.168.1.1 - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" test 532' +] + +# ------------------ +# Fixtures générales +# ------------------ + +@pytest.fixture +def parseur_arguments_cli(): + """ + Fixture pour initialiser le parseur d'arguments CLI. + Returns: + ParseurArgumentsCLI: Une instance de la classe :class:`ParseurArgumentsCLI`. + """ + return ParseurArgumentsCLI() + +@pytest.fixture() +def log_apache(tmp_path): + """ + Fixture pour créer et récupérer un fichier de log Apache temporaire. + Elle permet de générer un fichier de log Apache temporaire contenant + soit des lignes valides, soit des lignes invalides selon le paramètre fourni. + Args: + tmp_path (Path): Chemin temporaire fourni par pytest. + Returns: + Callable[[bool], Path]: Une fonction qui crée et retourne le chemin + du fichier de log temporaire. + """ + def _creer_log(valide): + """ + Crée un fichier de log Apache temporaire. + Args: + valide (bool): Si ``True``, le fichier contient des lignes de log valides. + Sinon, il contient des lignes invalides. + Returns: + Path: Le chemin du fichier de log temporaire créé. + """ + contenu = ( + "\n".join(lignes_valides) + if valide == True + else "\n".join(lignes_invalides) + ) + fichier_temp = tmp_path / "access.log" + fichier_temp.write_text(contenu) + return fichier_temp + return _creer_log + +@pytest.fixture +def parseur_log_apache(log_apache, request): + """ + Fixture pour initialiser un parseur de fichier de log Apache. + Args: + log_apache: La fixture pour initialiser un fichier temporaire. + request: Paramètre de la fonction. Si il est égale à ``False``, cette fixture + retourne un parseur de log Apache qui analyse un fichier avec un format + invalide. Sinon, retourne un parseur de log Apache qui analyse un fichier + avec un format valide. + Returns: + ParseurLogApache: Une instance de la classe :class:`ParseurLogApache`. + """ + if hasattr(request, "param") and request.param == False: + return ParseurLogApache(log_apache(False)) + return ParseurLogApache(log_apache(True)) + +@pytest.fixture() +def fichier_log_apache(parseur_log_apache): + """ + Fixture pour initialiser une représentation d'un fichier de log Apache. + Cette représentation comprend par défaut les entrées parsées de la liste + ``lignes_valides``. + Returns: + FichierLogApache: Une instance de la classe :class:`FichierLogApache`. + """ + return parseur_log_apache.parse_fichier() + +@pytest.fixture() +def analyseur_log_apache(fichier_log_apache): + """ + Fixture pour initialiser un analyseur statistique de fichier de log Apache. + Le fichier qu'analyse cet analyseur comprend par défaut les entrées parsées de la liste + ``lignes_valides``. + Returns: + AnalyseurLogApache: Une instance de la classe :class:`AnalyseurLogApache`. + """ + return AnalyseurLogApache(fichier_log_apache) \ No newline at end of file diff --git a/tests/test_analyseur_log_apache.py b/tests/test_analyseur_log_apache.py new file mode 100644 index 0000000..4618c01 --- /dev/null +++ b/tests/test_analyseur_log_apache.py @@ -0,0 +1,204 @@ +""" +Modules des tests unitaires pour l'analyse statistique d'un fichier de log Apache. +""" + +import pytest +from parse.fichier_log_apache import FichierLogApache +from analyse.analyseur_log_apache import AnalyseurLogApache + +# Tests unitaires + +@pytest.mark.parametrize("fichier, nombre_par_top", [ + (False, 3), + (FichierLogApache("test.log"), False) +]) +def test_analyseur_exception_type_invalide(fichier, nombre_par_top): + """ + Vérifie que la classe AnalyseurLogApache lève une :class:`TypeError` si les types des + paramètres du constructeur sont invalides. + + Scénarios testés: + - Type incorrect pour le paramètre ``fichier``. + - Type incorrect pour le paramètre ``nombre_par_top``. + + Args: + fichier (any): Représentation du fichier log. + nombre_par_top (any): Nombre maximum d'éléments dans le top classement. + """ + with pytest.raises(TypeError): + analyseur = AnalyseurLogApache(fichier, nombre_par_top) + +def test_analyseur_exception_valeur_nombre_par_top_invalide(): + """ + Vérifie que la classe AnalyseurLogApache lève une :class:`ValueError` si le + paramètre ``nombre_par_top`` du constructeur est un entier négatif. + + Scénarios testés: + - Nombre négatif pour ``nombre_par_top``. + """ + with pytest.raises(ValueError): + analyseur = AnalyseurLogApache(FichierLogApache("test.log"), -4) + +@pytest.mark.parametrize("liste_elements, nom_element, mode_top_classement", [ + (0, "test", True), + ([], 0, True), + ([], "test", 0) +]) +def test_analyseur_exception_repartition_elements_type_invalide(analyseur_log_apache, + liste_elements, + nom_element, + mode_top_classement): + """ + Vérifie que _get_repartition_elements lève une :class:`TypeError` si les types sont invalides. + + Scénarios testés: + - ``liste_elements`` n'est pas une ``list``. + - ``nom_element`` n'est pas un ``str``. + - ``mode_top_classement`` n'est pas un ``bool``. + + Args: + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe ``ParseurLogApache``. + liste_elements (any): Liste des éléments à répartir. + nom_element (any): Nom des éléments à analyser. + mode_top_classement (any): Mode top-classement activé ou non. + """ + with pytest.raises(TypeError): + analyseur_log_apache._get_repartition_elements(liste_elements, + nom_element, + mode_top_classement) + +@pytest.mark.parametrize("liste_elements, nom_element, resultat_attendu", [ + ([1] * 6 + [2] * 4, "chiffre", [ + {"chiffre": 1, "total": 6, "taux": 60.0}, + {"chiffre": 2, "total": 4, "taux": 40.0} + ]), + (["GET"] * 1 + ["POST"] * 7 + ["DELETE"] * 2, "methode", [ + {"methode": "POST", "total": 7, "taux": 70.0}, + {"methode": "DELETE", "total": 2, "taux": 20.0}, + {"methode": "GET", "total": 1, "taux": 10.0} + ]), +]) +def test_analyseur_repartition_elements_valide(analyseur_log_apache, + liste_elements, + nom_element, + resultat_attendu): + """ + Vérifie la répartition correcte des éléments dans ``_get_repartition_elements``. + + Scénarios testés: + - Répartition de chiffres avec deux valeurs fréquentes. + - Répartition de méthodes HTTP avec trois valeurs fréquentes. + + Asserts: + - La liste est triée dans l'ordre attendu. + - Le nombre d'éléments dans le résultat correspond à celui attendu. + - Les totaux et les taux sont correctement calculés. + + Args: + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe ``ParseurLogApache``. + liste_elements (list): Liste des éléments à analyser. + nom_element (str): Nom de l'élément à analyser. + resultat_attendu (list): Résultat attendu après répartition. + """ + repartitions = analyseur_log_apache._get_repartition_elements( + liste_elements, nom_element + ) + assert len(repartitions) == len(resultat_attendu) + for repartition, resultat in zip(repartitions, resultat_attendu): + assert repartition[nom_element] == resultat[nom_element] + assert repartition["total"] == resultat["total"] + assert repartition["taux"] == resultat["taux"] + +@pytest.mark.parametrize("liste_elements, nom_element, nombre_top, resultat_attendu", [ + ([1] * 6 + [2] * 4, "chiffre", 1, [ + {"chiffre": 1, "total": 6, "taux": 60.0} + ]), + (["GET"] * 1 + ["POST"] * 7 + ["DELETE"] * 2, "methode", 2, [ + {"methode": "POST", "total": 7, "taux": 70.0}, + {"methode": "DELETE", "total": 2, "taux": 20.0} + ]), +]) +def test_analyseur_repartition_mode_top_elements_valide(analyseur_log_apache, + liste_elements, + nom_element, + nombre_top, + resultat_attendu): + """ + Vérifie que ``_get_repartition_elements`` retourne les top éléments correctement. + + Scénarios testés: + - Classement des chiffres avec un seul élément dans le top. + - Classement des méthodes HTTP avec deux éléments dans le top. + + Asserts: + - Le nombre de top éléments retournés correspond à ``nombre_top``. + - Les totaux et les taux sont correctement calculés. + + Args: + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe ``ParseurLogApache``. + liste_elements (list): Liste des éléments à analyser. + nom_element (str): Nom de l'élément à analyser. + nombre_top (int): Nombre maximum d'éléments à retourner. + resultat_attendu (list): Résultat attendu. + """ + analyseur_log_apache.nombre_par_top = nombre_top + repartitions = analyseur_log_apache._get_repartition_elements( + liste_elements, nom_element, True + ) + assert len(repartitions) == len(resultat_attendu) + for repartition, resultat in zip(repartitions, resultat_attendu): + assert repartition[nom_element] == resultat[nom_element] + assert repartition["total"] == resultat["total"] + assert repartition["taux"] == resultat["taux"] + + +def test_analyseur_top_urls_valide(analyseur_log_apache): + """ + Vérifie que la méthode ``get_top_urls`` retourne correctement les URLs les plus fréquentées. + + Scénarios testés: + - Vérification du tri et du calcul du taux de fréquence. + + 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. + """ + top_urls = analyseur_log_apache.get_top_urls() + assert len(top_urls) == 2 + assert top_urls[0]["url"] == "/index.html" + assert top_urls[0]["total"] == 3 + assert top_urls[0]["taux"] == 60.0 + assert top_urls[1]["url"] == "/" + assert top_urls[1]["total"] == 2 + assert top_urls[1]["taux"] == 40.0 + +def test_analyseur_repartition_code_statut_htpp_valide(analyseur_log_apache): + """ + Vérifie que ``get_total_par_code_statut_http`` retourne la répartition correcte des codes HTTP. + + Scénarios testés: + - Vérification du tri et du calcul du taux de fréquence. + + 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() + assert len(repartition) == 2 + assert repartition[0]["code"] == 500 + assert repartition[0]["total"] == 4 + assert repartition[0]["taux"] == 80.0 + assert repartition[1]["code"] == 200 + assert repartition[1]["total"] == 1 + assert repartition[1]["taux"] == 20.0 \ No newline at end of file diff --git a/tests/test_parseur_arguments_cli.py b/tests/test_parseur_arguments_cli.py index 08a83cf..ac10351 100644 --- a/tests/test_parseur_arguments_cli.py +++ b/tests/test_parseur_arguments_cli.py @@ -3,19 +3,10 @@ """ import pytest -from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException +from cli.parseur_arguments_cli import ArgumentCLIException -# Données utilisées pour les tests unitaires -@pytest.fixture -def parseur_arguments_cli(): - """ - Fixture pour initialiser le parseur d'arguments CLI. - Retourne une instance de la classe ParseurArgumentsCLI pour être utilisée dans les tests. - Returns: - ParseurArgumentsCLI: Une instance de la classe ParseurArgumentsCLI. - """ - return ParseurArgumentsCLI() +# Données utilisées pour les tests unitaires chemins_valides = [ "fichier.log", diff --git a/tests/test_parseur_log_apache.py b/tests/test_parseur_log_apache.py index d217f6f..decad2a 100644 --- a/tests/test_parseur_log_apache.py +++ b/tests/test_parseur_log_apache.py @@ -5,73 +5,9 @@ import pytest from re import match from datetime import datetime, timezone, timedelta +from conftest import lignes_valides, lignes_invalides from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException -# Données utilisées pour les tests unitaires - -# Liste d'entrées valides -lignes_log_apache = [ - # Première entrée - '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 532', - # Deuxième entrée - '::1 - - [05/Mar/2025:16:59:43 +0100] "POST /backend/getConnexion.php HTTP/1.1" 200 20' - '"http://localhost/backend/connexion.php?titre=Connexion" "Mozilla/5.0 (Windows NT 10.0;' - ' Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"', - # Troisième entrée - '::1 - - [05/Mar/2025:16:59:43 +0100] "DELETE / HTTP/2.1" 200 20' -] - -# Liste d'entrées invalides -lignes_log_apache_invalides = [ - '', - 'Une ligne avec un format invalide !', - '::1 - - [05/Mar/2025:16:59:43] "DELETE / HTTP/2.1" 200 20', - '192.168.1.1 - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" test 532' -] - -@pytest.fixture() -def log_apache(tmp_path): - """ - Fixture pour créer et récupérer un fichier de log Apache temporaire. - Cette fixture permet de générer un fichier de log Apache temporaire contenant - soit des lignes valides, soit des lignes invalides selon le paramètre fourni. - Args: - tmp_path (Path): Chemin temporaire fourni par pytest. - Returns: - Callable[[bool], Path]: Une fonction qui crée et retourne le chemin - du fichier de log temporaire. - """ - def _creer_log(valide): - """ - Crée un fichier de log Apache temporaire. - Args: - valide (bool): Si True, le fichier contient des lignes de log valides. - Sinon, il contient des lignes invalides. - Returns: - Path: Le chemin du fichier de log temporaire créé. - """ - contenu = ( - "\n".join(lignes_log_apache) - if valide == True - else "\n".join(lignes_log_apache_invalides) - ) - fichier_temp = tmp_path / "access.log" - fichier_temp.write_text(contenu) - return fichier_temp - return _creer_log - -@pytest.fixture -def parseur_log_apache(log_apache, request): - """ - Fixture pour initialiser le parseur de log Apache. - Retourne une instance de la classe ParseurLogApache pour être utilisée dans les tests. - Returns: - ParseurLogApache: Une instance de la classe ParseurArgumentsCLI. - """ - if hasattr(request, "param") and request.param == False: - return ParseurLogApache(log_apache(False)) - return ParseurLogApache(log_apache(True)) - # Tests unitaires @@ -98,8 +34,8 @@ def test_exception_fichier_invalide(parseur_log_apache): with pytest.raises(FormatLogApacheInvalideException): fichier = parseur_log_apache.parse_fichier() -@pytest.mark.parametrize("ligne_log", lignes_log_apache_invalides) -def test_exception_entree_invalide(parseur_log_apache, ligne_log): +@pytest.mark.parametrize("ligne_invalide", lignes_invalides) +def test_exception_entree_invalide(parseur_log_apache, ligne_invalide): """ Vérifie qu'une exception est bien levée lorsque le format d'au moins une entrée est invalide dans un fichier de log Apache. @@ -111,7 +47,7 @@ def test_exception_entree_invalide(parseur_log_apache, ligne_log): None """ with pytest.raises(FormatLogApacheInvalideException): - parseur_log_apache.parse_entree(ligne_log) + parseur_log_apache.parse_entree(ligne_invalide) def test_nombre_entrees_valide(parseur_log_apache): """ @@ -123,7 +59,7 @@ def test_nombre_entrees_valide(parseur_log_apache): None """ fichier_log = parseur_log_apache.parse_fichier() - assert len(fichier_log.entrees) == len(lignes_log_apache) + assert len(fichier_log.entrees) == len(lignes_valides) @pytest.mark.parametrize("nom_information, retour_attendu", [ ("ip", "192.168.1.1"), From 9973aa126249a654d3f2e2c83aef41b7abb63154 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:19:23 +0100 Subject: [PATCH 35/47] Docs: Ajout de la documentation de la classe AnalyseurLogApache (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout des fichiers .rst pour ajouter la classe AnalyseurLogApache dans la documentation - Amélioration des docstrings de la classe AnalyseurLogApache pour correspondre au format Google et Sphinx --- app/analyse/analyseur_log_apache.py | 24 ++++++++++++++----- .../modules/analyse/analyseur_log_apache.rst | 7 ++++++ docs/source/modules/analyse/index_analyse.rst | 8 +++++++ docs/source/modules/index_modules.rst | 3 ++- 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 docs/source/modules/analyse/analyseur_log_apache.rst create mode 100644 docs/source/modules/analyse/index_analyse.rst diff --git a/app/analyse/analyseur_log_apache.py b/app/analyse/analyseur_log_apache.py index ee5c2b4..26e012d 100644 --- a/app/analyse/analyseur_log_apache.py +++ b/app/analyse/analyseur_log_apache.py @@ -10,6 +10,7 @@ class AnalyseurLogApache: """ Représente un analysateur pour faire une analyse statistique d'un fichier log Apache et créer des statistiques à partir de ce dernier. + Attributes: fichier (FichierLogApache): Le fichier de log Apache à analyser. nombre_par_top (int): Le nombre maximal d'éléments à inclure dans @@ -19,10 +20,12 @@ class AnalyseurLogApache: def __init__(self, fichier_log_apache: FichierLogApache, nombre_par_top: int = 3): """ Initialise un nouveau analysateur de fichier log Apache. + Args: fichier_log_apache (FichierLogApache): Le fichier à analyser. nombre_par_top (int): Le nombre maximal d'éléments à inclure dans les statistiques des classements (tops). Par défaut, sa valeur est égale à ``3``. + Raises: TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance de :class:`FichierLogApache` ou si l'argument ``nombre_par_top`` n'est pas un entier. @@ -43,12 +46,14 @@ def _get_repartition_elements(self, mode_top_classement: bool = False) -> list: """ Retourne le top 'n' des éléments qui apparaissent le plus dans la liste. + Args: liste_elements (list): La liste des éléments. nom_elements (str): Le nom des éléments. mode_top_classement (bool): Indique si la méthode doit retourner ou non le top 'n' des éléments les plus présents, où 'n' est égale à l'attribut :attr:`nombre_par_top`. Par défaut, ce mode est désactivé (valeur à ``False``). + Returns: list: Une liste de dictionnaires contenant, pour chaque élément : - Sa valeur. @@ -77,14 +82,16 @@ def _get_repartition_elements(self, def get_analyse_complete(self) -> dict: """ Retourne l'analyse complète du fichier de log Apache. + + L'analyse suit la structure suivante : + - chemin: chemin du fichier + - statistiques: + - requetes: + - top_urls: voir :meth:`get_top_urls` + - repartition_code_statut_http: voir :meth:`get_total_par_code_statut_http` + Returns: dict: L'analyse sous forme d'un dictionnaire. - Le dictionnaire suit la structure suivante: - - chemin: chemin du fichier - - statistiques: - - requetes: - - top_urls: voir :meth:`get_top_urls` - - repartition_code_statut_http: voir :meth:`get_total_par_code_statut_http` """ return { "chemin": self.fichier.chemin, @@ -100,6 +107,7 @@ def get_analyse_complete(self) -> dict: def get_total_entrees(self) -> int: """ Retourne le nombre total d'entrées dans le fichier. + Returns: int: Le nombre total d'entrées. """ @@ -108,11 +116,13 @@ def get_total_entrees(self) -> int: def get_top_urls(self) -> list: """ Retourne le top :attr:`nombre_par_top` des urls les plus demandées. + Returns: list: Une liste de dictionnaires où chaque clé contient : - url: L'URL demandée. - total: Le nombre total de fois où cette URL a été demandée. - taux: Le pourcentage de demandes correspondant à cette URL. + La liste est triée dans l'ordre décroissant du nombre total d'apparitions. """ return self._get_repartition_elements( @@ -124,11 +134,13 @@ 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é. + Returns: list: Une liste de dictionnaires où chaque clé contient : - code: Le code de statut http. - total: Le nombre total de fois où ce code a été demandée. - taux: Le pourcentage de demandes correspondant à ce code. + La liste est triée dans l'ordre décroissant du nombre total d'apparitions. """ return self._get_repartition_elements( diff --git a/docs/source/modules/analyse/analyseur_log_apache.rst b/docs/source/modules/analyse/analyseur_log_apache.rst new file mode 100644 index 0000000..69138a6 --- /dev/null +++ b/docs/source/modules/analyse/analyseur_log_apache.rst @@ -0,0 +1,7 @@ +AnalyseurLogApache +====================== + +.. automodule:: analyse.analyseur_log_apache + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/analyse/index_analyse.rst b/docs/source/modules/analyse/index_analyse.rst new file mode 100644 index 0000000..64ad521 --- /dev/null +++ b/docs/source/modules/analyse/index_analyse.rst @@ -0,0 +1,8 @@ +Analyse +=========== + +.. toctree:: + :maxdepth: 4 + + analyseur_log_apache.rst + \ No newline at end of file diff --git a/docs/source/modules/index_modules.rst b/docs/source/modules/index_modules.rst index 1a85a35..48c430e 100644 --- a/docs/source/modules/index_modules.rst +++ b/docs/source/modules/index_modules.rst @@ -8,4 +8,5 @@ Modules main.rst cli/index_cli.rst parse/index_parse.rst - donnees/index_donnees.rst \ No newline at end of file + donnees/index_donnees.rst + analyse/index_analyse.rst \ No newline at end of file From 7a8793309416bde17316fa5262acc525260e6bb4 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 29 Mar 2025 22:49:18 +0100 Subject: [PATCH 36/47] =?UTF-8?q?Test:=20Am=C3=A9lioration=20de=20la=20cou?= =?UTF-8?q?verture=20des=20tests=20unitaires=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de tests unitaires afin d'augmenter la couverture des tests unitaires sur le code - Amélioration de la docstring des fonctions de test unitaire - Ajout de la nouvelle dépendance 'pytest-mock' qui est nécessaire pour les tests unitaires du module main.py - Ajout d'une fonction main dans main.py pour faciliter les tests unitaires - Suppression d'une vérification de type dans parseur_log_apache.py qui ne pouvait jamais être atteinte grâce au regex --- .github/workflows/tests.yaml | 1 + app/main.py | 9 ++- app/parse/parseur_log_apache.py | 6 +- tests/conftest.py | 39 +++++++++- tests/test_analyseur_log_apache.py | 84 ++++++++++++++++++-- tests/test_donnees_log_apache.py | 116 ++++++++++++++++++++++++++-- tests/test_main.py | 58 ++++++++++++++ tests/test_parseur_arguments_cli.py | 99 +++++++++++++++++------- tests/test_parseur_log_apache.py | 88 ++++++++++++++------- 9 files changed, 421 insertions(+), 79 deletions(-) create mode 100644 tests/test_main.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index cab9cb8..a225206 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -33,6 +33,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pip install pytest-cov + pip install pytest-mock # Étape 4 : Lancer les tests unitaires - name: Lancer les tests unitaires diff --git a/app/main.py b/app/main.py index a1b7aaf..bf72c90 100644 --- a/app/main.py +++ b/app/main.py @@ -8,7 +8,10 @@ from analyse.analyseur_log_apache import AnalyseurLogApache -if __name__ == "__main__": +def main(): + """ + Point d'entrée de l'application. + """ colorama.init() print(colorama.Style.DIM + r""" .-. .-') .-') .-') _ ('-. _ .-') ,---. @@ -41,3 +44,7 @@ print(f"Erreur dans l'analyse du log Apache !\n{ex}") except Exception as ex: print(f"Erreur interne !\n{ex}") + + +if __name__ == "__main__": + main() diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py index a439936..a296e80 100644 --- a/app/parse/parseur_log_apache.py +++ b/app/parse/parseur_log_apache.py @@ -114,11 +114,7 @@ def parse_entree(self, entree): # Récupération des informations liées à la réponse code_statut = self.get_information_entree(resultat_analyse, "code_status") - if code_statut: - code_statut = int(code_statut) - - if code_statut is None: - raise FormatLogApacheInvalideException("Le code de statut HTTP est obligatoire.") + code_statut = int(code_statut) taille_octets = self.get_information_entree(resultat_analyse, "taille_octets") if taille_octets: diff --git a/tests/conftest.py b/tests/conftest.py index bf31bf0..e806291 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,11 @@ '::1 - - [05/Mar/2025:16:59:43] "DELETE / HTTP/2.1" 200 20', - '192.168.1.1 - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" test 532' + '- - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" 500 532', + + '::1 - - - "GET /index.html HTTP/1.1" 500 532', + + '::1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" - 120' ] # ------------------ @@ -47,6 +51,7 @@ def parseur_arguments_cli(): """ Fixture pour initialiser le parseur d'arguments CLI. + Returns: ParseurArgumentsCLI: Une instance de la classe :class:`ParseurArgumentsCLI`. """ @@ -58,8 +63,10 @@ def log_apache(tmp_path): Fixture pour créer et récupérer un fichier de log Apache temporaire. Elle permet de générer un fichier de log Apache temporaire contenant soit des lignes valides, soit des lignes invalides selon le paramètre fourni. + Args: tmp_path (Path): Chemin temporaire fourni par pytest. + Returns: Callable[[bool], Path]: Une fonction qui crée et retourne le chemin du fichier de log temporaire. @@ -67,9 +74,11 @@ def log_apache(tmp_path): def _creer_log(valide): """ Crée un fichier de log Apache temporaire. + Args: valide (bool): Si ``True``, le fichier contient des lignes de log valides. Sinon, il contient des lignes invalides. + Returns: Path: Le chemin du fichier de log temporaire créé. """ @@ -87,12 +96,14 @@ def _creer_log(valide): def parseur_log_apache(log_apache, request): """ Fixture pour initialiser un parseur de fichier de log Apache. + Args: log_apache: La fixture pour initialiser un fichier temporaire. request: Paramètre de la fonction. Si il est égale à ``False``, cette fixture retourne un parseur de log Apache qui analyse un fichier avec un format invalide. Sinon, retourne un parseur de log Apache qui analyse un fichier avec un format valide. + Returns: ParseurLogApache: Une instance de la classe :class:`ParseurLogApache`. """ @@ -106,17 +117,43 @@ def fichier_log_apache(parseur_log_apache): Fixture pour initialiser une représentation d'un fichier de log Apache. Cette représentation comprend par défaut les entrées parsées de la liste ``lignes_valides``. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurLogApache`. + Returns: FichierLogApache: Une instance de la classe :class:`FichierLogApache`. """ return parseur_log_apache.parse_fichier() +@pytest.fixture() +def entree_log_apache(fichier_log_apache): + """ + Fixture pour initialiser une représentation d'une entrée d'un fichier de log Apache. + Cette représentation comprend par défaut les informations de la première ligne de + ``lignes_valides``. + + Args: + fichier_log_apache (FichierLogApache): Fixture pour l'instance + de la classe :class:`FichierLogApache`. + + Returns: + EntreeLogApache: Une instance de la classe :class:`EntreeLogApache`. + """ + return fichier_log_apache.entrees[0] + @pytest.fixture() def analyseur_log_apache(fichier_log_apache): """ Fixture pour initialiser un analyseur statistique de fichier de log Apache. Le fichier qu'analyse cet analyseur comprend par défaut les entrées parsées de la liste ``lignes_valides``. + + Args: + fichier_log_apache (FichierLogApache): Fixture pour l'instance + de la classe :class:`FichierLogApache`. + Returns: AnalyseurLogApache: Une instance de la classe :class:`AnalyseurLogApache`. """ diff --git a/tests/test_analyseur_log_apache.py b/tests/test_analyseur_log_apache.py index 4618c01..5defd19 100644 --- a/tests/test_analyseur_log_apache.py +++ b/tests/test_analyseur_log_apache.py @@ -6,13 +6,14 @@ from parse.fichier_log_apache import FichierLogApache from analyse.analyseur_log_apache import AnalyseurLogApache + # Tests unitaires @pytest.mark.parametrize("fichier, nombre_par_top", [ (False, 3), (FichierLogApache("test.log"), False) ]) -def test_analyseur_exception_type_invalide(fichier, nombre_par_top): +def test_analyseur_log_exception_type_invalide(fichier, nombre_par_top): """ Vérifie que la classe AnalyseurLogApache lève une :class:`TypeError` si les types des paramètres du constructeur sont invalides. @@ -21,6 +22,9 @@ def test_analyseur_exception_type_invalide(fichier, nombre_par_top): - Type incorrect pour le paramètre ``fichier``. - Type incorrect pour le paramètre ``nombre_par_top``. + Asserts: + - Une exception :class:`TypeError` est levée. + Args: fichier (any): Représentation du fichier log. nombre_par_top (any): Nombre maximum d'éléments dans le top classement. @@ -28,13 +32,16 @@ def test_analyseur_exception_type_invalide(fichier, nombre_par_top): with pytest.raises(TypeError): analyseur = AnalyseurLogApache(fichier, nombre_par_top) -def test_analyseur_exception_valeur_nombre_par_top_invalide(): +def test_analyseur_log_exception_valeur_nombre_par_top_invalide(): """ - Vérifie que la classe AnalyseurLogApache lève une :class:`ValueError` si le + Vérifie que la classe AnalyseurLogApache lève une exception si le paramètre ``nombre_par_top`` du constructeur est un entier négatif. Scénarios testés: - Nombre négatif pour ``nombre_par_top``. + + Asserts: + - Une exception :class:`ValueError` est levée. """ with pytest.raises(ValueError): analyseur = AnalyseurLogApache(FichierLogApache("test.log"), -4) @@ -49,12 +56,15 @@ def test_analyseur_exception_repartition_elements_type_invalide(analyseur_log_ap nom_element, mode_top_classement): """ - Vérifie que _get_repartition_elements lève une :class:`TypeError` si les types sont invalides. + Vérifie que _get_repartition_elements lève une exception si les types sont invalides. Scénarios testés: - - ``liste_elements`` n'est pas une ``list``. - - ``nom_element`` n'est pas un ``str``. - - ``mode_top_classement`` n'est pas un ``bool``. + - Type incorrect pour le paramètre ``liste_elements``. + - Type incorrect pour le paramètre ``nom_element``. + - Type incorrect pour le paramètre ``mode_top_classement``. + + Asserts: + - Une exception :class:`TypeError` est levée. Args: analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance @@ -201,4 +211,62 @@ def test_analyseur_repartition_code_statut_htpp_valide(analyseur_log_apache): assert repartition[0]["taux"] == 80.0 assert repartition[1]["code"] == 200 assert repartition[1]["total"] == 1 - assert repartition[1]["taux"] == 20.0 \ No newline at end of file + assert repartition[1]["taux"] == 20.0 + +@pytest.mark.parametrize("nombre_entrees", [ + (0), (3), (100) +]) +def test_analyseur_get_total_entrees_valide(analyseur_log_apache, + fichier_log_apache, + entree_log_apache, + nombre_entrees): + """ + Vérifie que ``get_total_entrees`` retourne le nombre correcte d'entrées dans le fichier. + + Scénarios testés: + - Vérification avec des fichiers avec des nombre d'entrées différents. + + Asserts: + - La méthode renvoie le nombre d'éléments dans la liste. + + Args: + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe :class:`AnalyseurLogApache`. + fichier_log_apache (FichierLogApache): Fixture pour l'instance + de la classe :class:`FichierLogApache`. + entree_log_apache (EntreeLogApache): Fixture pour l'instance + de la classe :class:`EntreeLogApache`. + nombre_entrees (int): Le nombre total d'entrées dans le fichier. + """ + fichier_log_apache.entrees = [entree_log_apache] * nombre_entrees + assert analyseur_log_apache.get_total_entrees() == nombre_entrees + +def test_analyseur_get_analyse_complete_valide(analyseur_log_apache): + """ + Vérifie que ``get_analyse_complete`` retourne un rapport de l'analyse correct + qui se base sur le retour des autres méthodes. + + Scénarios testés: + - Vérification du rapport de l'analyse. + + Asserts: + - Les éléments du rapport se basent sur les mêmes valeurs que les autres méthodes. + + Args: + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe :class:`AnalyseurLogApache`. + fichier_log_apache (FichierLogApache): Fixture pour l'instance + de la classe :class:`FichierLogApache`. + entree_log_apache (EntreeLogApache): Fixture pour l'instance + de la classe :class:`EntreeLogApache`. + nombre_entrees (int): Le nombre total d'entrées dans le fichier. + """ + analyse = analyseur_log_apache.get_analyse_complete() + assert analyse["chemin"] == analyseur_log_apache.fichier.chemin + statistiques = analyse["statistiques"] + assert statistiques["total_entrees"] == analyseur_log_apache.get_total_entrees() + statistiques_requetes = statistiques["requetes"] + assert statistiques_requetes["top_urls"] == analyseur_log_apache.get_top_urls() + assert (statistiques_requetes["repartition_code_statut_http"] + == analyseur_log_apache.get_total_par_code_statut_http()) + \ No newline at end of file diff --git a/tests/test_donnees_log_apache.py b/tests/test_donnees_log_apache.py index 81778cd..1aa2f53 100644 --- a/tests/test_donnees_log_apache.py +++ b/tests/test_donnees_log_apache.py @@ -1,6 +1,5 @@ """ -Module des tests unitaires pour les classes contenant les données des logs Apache -(ClientInformations, RequetesInformations et ReponseInformations). +Module des tests unitaires pour les classes contenant les données des logs Apache. """ import pytest @@ -9,13 +8,30 @@ from donnees.requete_informations import RequeteInformations from donnees.reponse_informations import ReponseInformations + @pytest.mark.parametrize("adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur", [ ("192.168.0.1", "rfc", "utilisateur", "Mozilla/5.0") ]) -def test_client_informations_valide(adresse_ip, +def test_donnees_client_informations_valide(adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur): + """ + Vérifie que les arguments passés dans le constructeur de la classe :class:`ClientInformations` + sont bien récupérés. + + Scénarios testés: + - Création d'une instance avec des arguments valides. + + Asserts: + - La valeur des arguments sont bien conservées au bon endroit avec la bonne valeur. + + Args: + adresse_ip (str): Une adresse IP. + identifiant_rfc (str): Un identifiant RFC. + utilisateur (str): Un nom d'utilisateur. + agent_utilisateur (str): Un User-Agent. + """ client = ClientInformations( adresse_ip, identifiant_rfc, @@ -33,10 +49,29 @@ def test_client_informations_valide(adresse_ip, ("192.168.0.1", "rfc", False, "Mozilla/5.0"), ("192.168.0.1", "rfc", "utilisateur", False) ]) -def test_client_exception_type_invalide(adresse_ip, +def test_donnees_client_exception_type_invalide(adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur): + """ + Vérifie que la classe renvoie une erreur lorsque un argument de type invalide + est passé dans le constructeur. + + Scénarios testés: + - Type incorrect pour le paramètre ``adresse_ip``. + - Type incorrect pour le paramètre ``identifiant_rfc``. + - Type incorrect pour le paramètre ``utilisateur``. + - Type incorrect pour le paramètre ``agent_utilisateur``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + adresse_ip (any): Une adresse IP. + identifiant_rfc (any): Un identifiant RFC. + utilisateur (any): Un nom d'utilisateur. + agent_utilisateur (any): Un User-Agent. + """ with pytest.raises(TypeError): client = ClientInformations( adresse_ip, @@ -49,11 +84,28 @@ def test_client_exception_type_invalide(adresse_ip, (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), "POST", "essaie.fr/contact", "HTTP/1.2", "essaie.fr/accueil") ]) -def test_requete_informations_valide(horodatage, +def test_donnes_requete_informations_valide(horodatage, methode_http, url, protocole_http, ancienne_url): + """ + Vérifie que les arguments passés dans le constructeur de la classe :class:`RequeteInformations` + sont bien récupérés. + + Scénarios testés: + - Création d'une instance avec des arguments valides. + + Asserts: + - La valeur des arguments sont bien conservées au bon endroit avec la bonne valeur. + + Args: + horodatage (datetime): La date de reception de la requête. + methode_http (str): La méthode HTTP utilisée. + url (str): La ressource demandée. + protocole_http (str): Le protocole HTTP utilisé. + ancienne_url (str): L'ancienne ressource demandée. + """ requete = RequeteInformations( horodatage, methode_http, @@ -78,11 +130,32 @@ def test_requete_informations_valide(horodatage, (datetime(2012, 12, 12, 10, 10, 10, tzinfo=timezone(timedelta(hours=10))), "POST", "essaie.fr/contact", "HTTP/1.2", False), ]) -def test_requete_exception_type_invalide(horodatage, +def test_donnees_requete_exception_type_invalide(horodatage, methode_http, url, protocole_http, ancienne_url): + """ + Vérifie que la classe renvoie une erreur lorsque un argument de type invalide + est passé dans le constructeur. + + Scénarios testés: + - Type incorrect pour le paramètre ``horodatage``. + - Type incorrect pour le paramètre ``methode_http``. + - Type incorrect pour le paramètre ``url``. + - Type incorrect pour le paramètre ``protocole_http``. + - Type incorrect pour le paramètre ``ancienne_url``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + horodatage (any): La date de reception de la requête. + methode_http (any): La méthode HTTP utilisée. + url (any): La ressource demandée. + protocole_http (any): Le protocole HTTP utilisé. + ancienne_url (any): L'ancienne ressource demandée. + """ with pytest.raises(TypeError): requete = RequeteInformations( horodatage, @@ -95,8 +168,22 @@ def test_requete_exception_type_invalide(horodatage, @pytest.mark.parametrize("code_statut_http, taille_octets", [ (404, 50) ]) -def test_reponse_informations_valide(code_statut_http, +def test_donnes_reponse_informations_valide(code_statut_http, taille_octets): + """ + Vérifie que les arguments passés dans le constructeur de la classe :class:`ReponseInformations` + sont bien récupérés. + + Scénarios testés: + - Création d'une instance avec des arguments valides. + + Asserts: + - La valeur des arguments sont bien conservées au bon endroit avec la bonne valeur. + + Args: + code_statut_http (int): La code de retour. + taille_octets (int): La taille de la réponse en octets. + """ reponse = ReponseInformations( code_statut_http, taille_octets @@ -110,6 +197,21 @@ def test_reponse_informations_valide(code_statut_http, ]) def test_reponse_exception_type_invalide(code_statut_http, taille_octets): + """ + Vérifie que la classe renvoie une erreur lorsque un argument de type invalide + est passé dans le constructeur. + + Scénarios testés: + - Type incorrect pour le paramètre ``code_statut_http``. + - Type incorrect pour le paramètre ``taille_octets``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + code_statut_http (int): La code de retour. + taille_octets (int): La taille de la réponse en octets. + """ with pytest.raises(TypeError): reponse = ReponseInformations( code_statut_http, diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..cd3f474 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,58 @@ +""" +Module des tests unitaires pour le point d'entrée de l'application. +""" + +import pytest +import sys +from main import main +from cli.parseur_arguments_cli import ArgumentCLIException +from parse.parseur_log_apache import FormatLogApacheInvalideException + +@pytest.mark.parametrize("exception", [ + (ArgumentCLIException), + (FileNotFoundError), + (FormatLogApacheInvalideException), + (Exception) +]) +def test_main_gestion_exception(mocker, exception): + """ + Vérifie que les exceptions sont interceptées dans fichier principal. + + Scénarios testés: + - Vérification que les exceptions n'arrête pas le programme. + + Args: + mocker (any): Une fixture pour simuler des exceptions. + exception (any): L'exception à simuler. + """ + mocker.patch("main.ParseurArgumentsCLI", side_effect=exception) + main() + + +def test_main_succes(mocker): + """ + Vérifie le fonctionnement du fichier principal sans exception. + + Scénarios testés: + - Vérification que le fichier principal s'execute sans exception lors + d'un déroulement normal. + + Args: + mocker (any): Une fixture pour simuler des retours pour les classes + et méthodes dans main. + """ + # Mock des classes pour simuler un fonctionnement correct + mock_parseur_cli = mocker.patch("main.ParseurArgumentsCLI") + mock_parseur_cli.return_value.parse_args.return_value = mocker.MagicMock(chemin_log="test.log") + + mock_parseur_log = mocker.patch("main.ParseurLogApache") + mock_parseur_log.return_value.parse_fichier.return_value = mocker.MagicMock() + + mock_analyseur_log = mocker.patch("main.AnalyseurLogApache") + mock_analyseur_log.return_value.get_analyse_complete.return_value = {"chemin": "test.log"} + + # Vérifie qu'aucune exception n'est levée + try: + main() + except Exception: + pytest.fail("Aucune exception ne doit être levée ici") \ No newline at end of file diff --git a/tests/test_parseur_arguments_cli.py b/tests/test_parseur_arguments_cli.py index ac10351..6d34ee6 100644 --- a/tests/test_parseur_arguments_cli.py +++ b/tests/test_parseur_arguments_cli.py @@ -37,98 +37,137 @@ # Tests unitaires @pytest.mark.parametrize("arguments", arguments_invalides) -def test_exception_argument_inconnu(parseur_arguments_cli, arguments): +def test_parseur_cli_exception_argument_inconnu(parseur_arguments_cli, arguments): """ Vérifie qu'une erreur est retournée lorsque un argument est passé en CLI et qu'il n'est pas reconnu. + + Scénarios testés: + - Demande de parsage d'arguments avec des arguments invalides. + + Asserts: + - Une exception :class:`ArgumentCLIException` est levée lorsque un argument est invalide. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurArgumentsCLI. - chemin_log (str): Un chemin de fichier valide. - Returns: - None + de la classe :class:`ParseurArgumentsCLI`. + argument (list): Une liste avec des arguments. """ with pytest.raises(ArgumentCLIException): arguments = parseur_arguments_cli.parse_args(args=arguments) @pytest.mark.parametrize("chemin_log", chemins_valides) -def test_recuperation_chemin_log_valide(parseur_arguments_cli, chemin_log): +def test_parseur_cli_recuperation_chemin_log_valide(parseur_arguments_cli, chemin_log): """ Vérifie que le chemin du log Apache fourni depuis la ligne de commande est bien récupéré par le parseur. + + Scénarios testés: + - Demande de parsage d'un chemin de log. + + Asserts: + - La valeur du chemin de log est bien récupérée et conforme à l'entrée. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurArgumentsCLI. + de la classe :class:`ParseurArgumentsCLI`. chemin_log (str): Un chemin de fichier valide. - Returns: - None """ arguments = parseur_arguments_cli.parse_args(args=[chemin_log]) assert arguments.chemin_log == chemin_log @pytest.mark.parametrize("chemin_log", chemins_invalides) -def test_exception_chemin_log_invalide(parseur_arguments_cli, chemin_log): +def test_parseur_cli_exception_chemin_log_invalide(parseur_arguments_cli, chemin_log): """ Vérifie qu'une erreur est retournée lorsque le chemin du log Apache fourni contient au moins un caractère non autorisé. + + Scénarios testés: + - Demande de parsage d'un chemin de log avec un format invalide. + + Asserts: + - Une exception :class:`ArgumentCLIException` est levée. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurArgumentsCLI. + de la classe :class:`ParseurArgumentsCLI`. chemin_log (str): Un chemin de fichier invalide. - Returns: - None """ with pytest.raises(ArgumentCLIException): parseur_arguments_cli.parse_args(args=[chemin_log]) @pytest.mark.parametrize("chemin_sortie", sorties_valides) -def test_recuperation_chemin_sortie_valide(parseur_arguments_cli, chemin_sortie): +def test_parseur_cli_recuperation_chemin_sortie_valide(parseur_arguments_cli, chemin_sortie): """ Vérifie que le chemin du fichier de sortie JSON fourni depuis la ligne de commande est bien récupéré par le parseur. + + Scénarios testés: + - Demande de parsage d'un chemin de fichier de sortie. + + Asserts: + - La valeur du chemin de sortie est bien récupérée et conforme à l'entrée. + Args: - parseur_arguments_cli: Fixture pour l'instance de la classe ParseurArgumentsCLI - Returns: - None + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurArgumentsCLI`. + chemin_sortie (str): Un chemin de fichier de sortie valide. """ arguments = parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", chemin_sortie]) assert arguments.sortie == chemin_sortie @pytest.mark.parametrize("chemin_sortie", chemins_invalides) -def test_exception_chemin_sortie_invalide(parseur_arguments_cli, chemin_sortie): +def test_parseur_cli_exception_chemin_sortie_invalide(parseur_arguments_cli, chemin_sortie): """ Vérifie qu'une erreur est retournée lorsque le chemin du fichier de sortie fourni contient au moins un caractère non autorisé. + + Scénarios testés: + - Demande de parsage d'un chemin de fichier de sortie invalide. + + Asserts: + - Une exception :class:`ArgumentCLIException` est levée. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurArgumentsCLI. - chemin_sortie (str): Un chemin de fichier invalide. - Returns: - None + de la classe :class:`ParseurArgumentsCLI`. + chemin_sortie (str): Un chemin de fichier de fichier invalide. """ with pytest.raises(ArgumentCLIException): parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", chemin_sortie]) -def test_recuperation_chemin_sortie_defaut_valide(parseur_arguments_cli): +def test_parseur_cli_recuperation_chemin_sortie_defaut_valide(parseur_arguments_cli): """ Vérifie que le chemin du fichier de sortie JSON par défaut est bien appliqué lorsque aucun chemin de sortie n'est donné. + + Scénarios testés: + - Demande de parsage avec aucun fichier de sortie indiqué. + + Asserts: + - La bonne valeur par défaut pour le chemin de sortie à été appliquée. + Args: - parseur_arguments_cli: Fixture pour l'instance de la classe ParseurArgumentsCLI - Returns: - None + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurArgumentsCLI`. """ argument = parseur_arguments_cli.parse_args(args=["fichier.txt"]) assert argument.sortie == "./analyse-log-apache.json" -def test_verification_extention_chemin_sortie(parseur_arguments_cli): +def test_parseur_cli_verification_extention_chemin_sortie(parseur_arguments_cli): """ Vérifie qu'une erreur est retournée lorsque le fichier de sortie fourni ne possède pas l'extension '.json'. + + Scénarios testés: + - Demande de parsage d'un fichier de sortie qui n'est pas un fichier .json. + + Asserts: + - Une exception :class:`ArgumentCLIException` est levée. + Args: - parseur_arguments_cli: Fixture pour l'instance de la classe ParseurArgumentsCLI - Returns: - None + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurArgumentsCLI`. """ with pytest.raises(ArgumentCLIException): parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", "invalide.txt"]) diff --git a/tests/test_parseur_log_apache.py b/tests/test_parseur_log_apache.py index decad2a..fb6f1ce 100644 --- a/tests/test_parseur_log_apache.py +++ b/tests/test_parseur_log_apache.py @@ -11,52 +11,72 @@ # Tests unitaires -def test_exception_fichier_invalide(): +def test_parseur_log_exception_fichier_introuvable(): """ - Vérifie qu'une exception est bien levée lorsque le fichier n'existe pas. - Returns: - None + Vérifie qu'une exception est bien levée lorsque le chemin du fichier + passé dans le constructeur n'est pas trouvé. + + Scénarios testés: + - Création d'une instance avec un chemin invalide. + + Asserts: + - Une exception :class:`FileNotFoundError` est levée. """ with pytest.raises(FileNotFoundError): parseur = ParseurLogApache("fichier/existe/pas.txt") @pytest.mark.parametrize("parseur_log_apache", [False], indirect=["parseur_log_apache"]) -def test_exception_fichier_invalide(parseur_log_apache): +def test_parseur_log_exception_fichier_invalide(parseur_log_apache): """ Vérifie qu'une exception est bien levée lorsque le format d'un fichier n'est pas valide (une entrée invalide). + + Scénarios testés: + - Parsage d'un fichier invalide. + + Asserts: + - Une exception :class:`FormatLogApacheInvalideException` est levée. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurLogApache. - Returns: - None + de la classe :class:`ParseurLogApache`. """ with pytest.raises(FormatLogApacheInvalideException): fichier = parseur_log_apache.parse_fichier() @pytest.mark.parametrize("ligne_invalide", lignes_invalides) -def test_exception_entree_invalide(parseur_log_apache, ligne_invalide): +def test_parseur_log_exception_entree_invalide(parseur_log_apache, ligne_invalide): """ - Vérifie qu'une exception est bien levée lorsque le format d'au moins une - entrée est invalide dans un fichier de log Apache. + Vérifie qu'une exception est bien levée lorsque le format d'une entrée + est invalide. + + Scénarios testés: + - Parsage d'une entrée invalide. + + Asserts: + - Une exception :class:`FormatLogApacheInvalideException` est levée. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurLogApache. - ligne_log (str): L'entrée à analyser. - Returns: - None + de la classe :class:`ParseurLogApache`. + ligne_invalide (str): L'entrée à analyser. """ with pytest.raises(FormatLogApacheInvalideException): parseur_log_apache.parse_entree(ligne_invalide) -def test_nombre_entrees_valide(parseur_log_apache): +def test_parseur_log_nombre_entrees_valide(parseur_log_apache): """ Vérifie que le nombre d'entrées trouvé correspond au nombre de ligne dans le log. + + Scénarios testés: + - Parsage d'un fichier et récupération du nombre d'entrées dans ce dernier. + + Asserts: + - Le nombre d'entrées récupéré est égale au nombre attendu. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurLogApache. - Returns: - None + de la classe :class:`ParseurLogApache`. """ fichier_log = parseur_log_apache.parse_fichier() assert len(fichier_log.entrees) == len(lignes_valides) @@ -67,17 +87,26 @@ def test_nombre_entrees_valide(parseur_log_apache): ("horodatage", "12/Jan/2025:10:15:32 +0000"), ("Existe pas !", None) ]) -def test_regex_recuperation_information_entree(parseur_log_apache, nom_information, retour_attendu): +def test_parseur_log_regex_recuperation_information_entree(parseur_log_apache, + nom_information, + retour_attendu): """ Vérifie que la récupération des informations à partir d'un résultat de regex fonctionne - correctement et que toutes valeurs introuvables ou égales à - renvoient None. + correctement et que toutes valeurs introuvables ou égales à - renvoient None. + + Scénarios testés: + - Récupération d'une information avec une valeur. + - Récupération d'une information sans valeur (égale à -). + - Récupération d'une information inexistante. + + Asserts: + - La valeur retournée est égale à celle attendue. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurLogApache. + de la classe :class:`ParseurLogApache`. nom_information (str): Nom de l'information à récupérer. retour_attendu (Union[None, str]): La valeur attendue de l'information. - Returns: - None """ ligne = '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" 200 532' analyse = match(parseur_log_apache.PATTERN_ENTREE_LOG_APACHE, ligne) @@ -88,11 +117,16 @@ def test_parsage_entree_valide(parseur_log_apache): """ Vérifie qu'une entrée est correctement analysée et que les informations partent au bon endroit avec le bon typage. + + Scénarios testés: + - Parsage d'une entrée valide et récupération des valeurs trouvées. + + Asserts: + - Les valeurs sont égales à celles attendues. + Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance - de la classe ParseurLogApache. - Returns: - None + de la classe :class:`ParseurLogApache`. """ ligne = '192.168.1.1 - - [12/Jan/2025:10:15:32 +0000] "GET /index.html HTTP/1.1" ' \ '200 532 "/home" "Chrome/133.0.0.0"' From 9a74e6c666028da8b6997821d97f1aa2b5a0c16d Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:06:44 +0100 Subject: [PATCH 37/47] Fix: Ajout de colorama pour corriger l'import dans les tests unitaires dans le workflow (#29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de la dépendance colorama dans le workflow tests.yaml qui était nécessaire dans les tests unitaires du module main.py --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a225206..b315e8c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -31,6 +31,7 @@ jobs: - name: Installer les dépendances run: | python -m pip install --upgrade pip + pip install colorama pip install pytest pip install pytest-cov pip install pytest-mock From df5260756824ba083a8ac63d4e2d54ecd8e7bd92 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sun, 30 Mar 2025 13:12:35 +0200 Subject: [PATCH 38/47] =?UTF-8?q?Feat:=20Ajout=20de=20l'exportation=20des?= =?UTF-8?q?=20donn=C3=A9es=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création du dossier export - Création de la classe Exporteur dans le module exporteur.py - Ajout de l'exception ExportationException - Utilisation de l'exportation dans le module main.py - Ajout des tests unitaires pour la classe Exporteur - Ajout de la documentation pour la classe Exporteur --- app/export/exporteur.py | 72 +++++++++++++ app/main.py | 5 + docs/source/modules/export/exporteur.rst | 7 ++ docs/source/modules/export/index_export.rst | 7 ++ docs/source/modules/index_modules.rst | 3 +- tests/conftest.py | 36 ++++++- tests/test_exporteur.py | 112 ++++++++++++++++++++ tests/test_main.py | 4 + 8 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 app/export/exporteur.py create mode 100644 docs/source/modules/export/exporteur.rst create mode 100644 docs/source/modules/export/index_export.rst create mode 100644 tests/test_exporteur.py diff --git a/app/export/exporteur.py b/app/export/exporteur.py new file mode 100644 index 0000000..2518e17 --- /dev/null +++ b/app/export/exporteur.py @@ -0,0 +1,72 @@ +""" +Module pour l'exportation des données. +""" + +from os.path import abspath, dirname, isdir +from json import dump + + +class Exporteur: + """ + Représente un exporteur de données pour exporter des données + vers un fichier de sortie. + + Attributes: + _chemin_sortie (str): Le chemin du fichier vers lequel + les données seront 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. + ExportationException: Exportation impossible à cause de + l'emplacement invalide du fichier de sortie. + """ + if not isinstance(chemin_sortie, str): + raise TypeError("Le chemin de sortie doit être une chaîne de caractère.") + chemin_sortie_absolue = abspath(chemin_sortie) + dossier_parent = dirname(chemin_sortie_absolue) + if not isdir(dossier_parent): + raise ExportationException(f"Impossible d'exporter vers le " + f"fichier {chemin_sortie}, son dossier parent " + f"{dossier_parent} n'existe pas.") + self._chemin_sortie = chemin_sortie + + def export_vers_json(self, donnees: dict): + """ + Export le dictionnaire fourni vers le :attr:`chemin de sortie`. + + Args: + donnees (dict): Le dictionnaire qui contient les données. + + Returns: + None + + Raises: + TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire. + ExportationException: Une erreur lors de l'écriture dans le fichier JSON. + """ + if not isinstance(donnees, dict): + raise TypeError("Les données à exporter doivent être sous une forme " + "de dictionnaire.") + + try: + with open(self._chemin_sortie, 'w') as fichier: + dump(donnees, fichier, indent=4) + except Exception as ex: + raise ExportationException(str(ex)) from ex + +class ExportationException(Exception): + """ + Représente une erreur lors de l'exportation de données. + """ + + def __init__(self, *args): + super().__init__(*args) diff --git a/app/main.py b/app/main.py index bf72c90..4833dc2 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException from analyse.analyseur_log_apache import AnalyseurLogApache +from export.exporteur import Exporteur, ExportationException def main(): @@ -36,12 +37,16 @@ def main(): analyseur_log = AnalyseurLogApache(fichier_log) analyse = analyseur_log.get_analyse_complete() # Exportation de l'analyse + exporteur = Exporteur(arguments_cli.sortie) + exporteur.export_vers_json(analyse) except ArgumentCLIException as ex: print(f"Erreur dans les arguments fournis !\n {ex}") except FileNotFoundError as ex: print(f"Erreur dans la recherche du log Apache !\n{ex}") except FormatLogApacheInvalideException as ex: print(f"Erreur dans l'analyse du log Apache !\n{ex}") + except ExportationException as ex: + print(f"Erreur dans l'exportation de l'analyse !\n{ex}") except Exception as ex: print(f"Erreur interne !\n{ex}") diff --git a/docs/source/modules/export/exporteur.rst b/docs/source/modules/export/exporteur.rst new file mode 100644 index 0000000..0c09299 --- /dev/null +++ b/docs/source/modules/export/exporteur.rst @@ -0,0 +1,7 @@ +Exporteur +========== + +.. automodule:: export.exporteur + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/export/index_export.rst b/docs/source/modules/export/index_export.rst new file mode 100644 index 0000000..27e733e --- /dev/null +++ b/docs/source/modules/export/index_export.rst @@ -0,0 +1,7 @@ +Export +=========== + +.. toctree:: + :maxdepth: 4 + + exporteur.rst diff --git a/docs/source/modules/index_modules.rst b/docs/source/modules/index_modules.rst index 48c430e..0760dfa 100644 --- a/docs/source/modules/index_modules.rst +++ b/docs/source/modules/index_modules.rst @@ -9,4 +9,5 @@ Modules cli/index_cli.rst parse/index_parse.rst donnees/index_donnees.rst - analyse/index_analyse.rst \ No newline at end of file + analyse/index_analyse.rst + export/index_export.rst \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index e806291..550738c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,9 +4,9 @@ import pytest from cli.parseur_arguments_cli import ParseurArgumentsCLI -from parse.fichier_log_apache import FichierLogApache from parse.parseur_log_apache import ParseurLogApache from analyse.analyseur_log_apache import AnalyseurLogApache +from export.exporteur import Exporteur # ----------------- @@ -98,8 +98,8 @@ def parseur_log_apache(log_apache, request): Fixture pour initialiser un parseur de fichier de log Apache. Args: - log_apache: La fixture pour initialiser un fichier temporaire. - request: Paramètre de la fonction. Si il est égale à ``False``, cette fixture + log_apache (Path): La fixture pour initialiser un fichier temporaire. + request (object): Paramètre de la fonction. Si il est égale à ``False``, cette fixture retourne un parseur de log Apache qui analyse un fichier avec un format invalide. Sinon, retourne un parseur de log Apache qui analyse un fichier avec un format valide. @@ -157,4 +157,32 @@ def analyseur_log_apache(fichier_log_apache): Returns: AnalyseurLogApache: Une instance de la classe :class:`AnalyseurLogApache`. """ - return AnalyseurLogApache(fichier_log_apache) \ No newline at end of file + return AnalyseurLogApache(fichier_log_apache) + +@pytest.fixture +def fichier_json(tmp_path): + """ + Fixture pour retourner un chemin de fichier JSON temporaire. + + Args: + tmp_path (Path): Chemin temporaire fourni par pytest. + + Returns: + Path: Un chemin de fichier JSON temporaire. + """ + fichier_temp = tmp_path / "sortie.json" + return fichier_temp + +@pytest.fixture +def exporteur(fichier_json): + """ + Fixture pour initialiser un exportateur de données. + + Args: + fichier_json (Path): Fixture pour initialiser + un chemin de fichier json temporaire. + + Returns: + Exporteur: Une instance de la classe :class:`Exportateur`. + """ + return Exporteur(str(fichier_json)) \ No newline at end of file diff --git a/tests/test_exporteur.py b/tests/test_exporteur.py new file mode 100644 index 0000000..f155bac --- /dev/null +++ b/tests/test_exporteur.py @@ -0,0 +1,112 @@ +""" +Module des tests unitaires pour l'exporteur de données. +""" + +import pytest +from json import load +from export.exporteur import Exporteur, ExportationException + +@pytest.mark.parametrize("chemin_sortie", [ + (0), (None), ([]) +]) +def test_exporteur_type_chemin_invalide(chemin_sortie): + """ + Vérifie que la classe renvoie une erreur lorsque un argument de type invalide + est passé dans le constructeur. + + Scénarios testés: + - Type incorrect pour le paramètre ``chemin_sortie``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + chemin_sortie (any): Le chemin de sortie utilisé dans le constructeur. + """ + with pytest.raises(TypeError): + exporteur = Exporteur(chemin_sortie) + +def test_exporteur_emplacement_inexistant(): + """ + Vérifie que la classe renvoie une erreur lorsque un chemin de fichier invalide + est passé dans le constructeur. + + Scénarios testés: + - Chemin invalide pour le paramètre ``chemin_sortie``. + + Asserts: + - Une exception :class:`ExportationException` est levée. + """ + with pytest.raises(ExportationException): + exporteur = Exporteur("dossier/inexistant/sortie.json") + +@pytest.mark.parametrize("donnees", [ + (0), (None), ([]) +]) +def test_exportateur_export_json_type_donnees_invalide(exporteur, donnees): + """ + Vérifie que la classe renvoie une erreur lorsque un argument de type invalide + est passé dans la méthode ``export_vers_json``. + + Scénarios testés: + - Type incorrect pour le paramètre ``données``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + donnees (any): Les données à exporter. + """ + with pytest.raises(TypeError): + exporteur.export_vers_json("type invalide") + +@pytest.mark.parametrize("exception", [ + (PermissionError("Pas les droits")), + (FileNotFoundError("Fichier non trouvé.")), + (Exception("Toutes exceptions")) +]) +def test_exportateur_export_json_exception_exportation(exporteur, mocker, exception): + """ + Vérifie que la classe renvoie l'exception :class:`ExportationException` lorsque + une erreur apparait durant l'exportation des données. + + Scénarios testés: + - Une exception :class:`PermissionError` survient. + - Une exception :class:`FileNotFoundError` survient. + - Une exception :class:`Exception` survient. + + Asserts: + - Une exception :class:`ExportationException` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + mocker (any): Une fixture pour simuler des exceptions. + donnees (any): Les données à exporter. + """ + mocker.patch("builtins.open", side_effect=exception) + with pytest.raises(ExportationException): + exporteur.export_vers_json({}) + +def test_exportateur_exportation_json_valide(exporteur, fichier_json): + """ + Vérifie que la méthode ``export_vers_json`` exporte correctement les données + vers une fichier. + + Scénarios testés: + - Exportation d'un dictionnaire lambda. + + Asserts: + - Le fichier est bien crée. + - Les données dans le fichier sont conformes à celles fournies. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + fichier_json (Path): Le chemin du fichier. + """ + donnees = {"cle": {"valeur": [1, 2, 3]}} + exporteur.export_vers_json(donnees) + assert fichier_json.exists() + with open(fichier_json, "r") as exportation: + contenu_exportation = load(exportation) + assert contenu_exportation == donnees \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index cd3f474..c2fb6e1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,11 +7,13 @@ from main import main from cli.parseur_arguments_cli import ArgumentCLIException from parse.parseur_log_apache import FormatLogApacheInvalideException +from export.exporteur import ExportationException @pytest.mark.parametrize("exception", [ (ArgumentCLIException), (FileNotFoundError), (FormatLogApacheInvalideException), + (ExportationException), (Exception) ]) def test_main_gestion_exception(mocker, exception): @@ -50,6 +52,8 @@ def test_main_succes(mocker): mock_analyseur_log = mocker.patch("main.AnalyseurLogApache") mock_analyseur_log.return_value.get_analyse_complete.return_value = {"chemin": "test.log"} + + mocker.patch("main.Exporteur") # Vérifie qu'aucune exception n'est levée try: From b6c84abe15b94c21814240b631677fcf8b180aed Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Thu, 3 Apr 2025 20:34:51 +0200 Subject: [PATCH 39/47] Feat: Afficheur CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .github/workflows/documentation.yaml | 1 + .github/workflows/tests.yaml | 5 +- app/cli/afficheur_cli.py | 210 +++++++++++++++++++++ app/main.py | 53 +++--- assets/animations.json | 18 ++ docs/source/modules/cli/afficheur_cli.rst | 7 + docs/source/modules/cli/index_cli.rst | 1 + tests/.coveragerc | 3 + tests/conftest.py | 5 + tests/test_afficheur_cli.py | 211 ++++++++++++++++++++++ tests/test_main.py | 4 +- 11 files changed, 490 insertions(+), 28 deletions(-) create mode 100644 app/cli/afficheur_cli.py create mode 100644 assets/animations.json create mode 100644 docs/source/modules/cli/afficheur_cli.rst create mode 100644 tests/.coveragerc create mode 100644 tests/test_afficheur_cli.py diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 0b22f51..4501cc5 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -28,6 +28,7 @@ jobs: python -m pip install --upgrade pip pip install sphinx pip install sphinx_rtd_theme --break-system-packages + pip install colorama - name: Construction de la documentation (avec Sphinx) run: | diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b315e8c..9497987 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -39,7 +39,8 @@ jobs: # Étape 4 : Lancer les tests unitaires - name: Lancer les tests unitaires run: | - 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 + cd tests + 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 # Étape 5 : Sauvegarder les artefacts - name: Sauvegarder les résultats de test @@ -48,4 +49,4 @@ jobs: with: if-no-files-found: error name: tests-resultats-python-${{ matrix.python-version }} # Nom de l'artefact - path: resultats_pytest # Eléments à sauvegarder + path: tests/resultats_pytest # Eléments à sauvegarder diff --git a/app/cli/afficheur_cli.py b/app/cli/afficheur_cli.py new file mode 100644 index 0000000..5083b13 --- /dev/null +++ b/app/cli/afficheur_cli.py @@ -0,0 +1,210 @@ +""" +Module pour les intéractions avec la ligne de commande. +""" + +import sys +from pathlib import Path +from json import load +from time import sleep +from random import choice +import colorama +import threading + +class AfficheurCLI: + """ + Représente une classe pour afficher des informations dans la ligne de commande. + + Attributes: + _thread_chargement (Union[None,Thread]): Le thread de l'animation de chargement. + _thread_chargement_termine (Event): L'évènement pour demander au thread de + l'animation de chargement de s'arrêter lorsque le chargement est terminé. + _thread_chargement_erreur (Event): L'évènement pour demander au thread de + l'animation de chargement de s'arrêter lorsque une erreur s'est produite. + _animations_actuelles (dict): Les éléments visuels pour l'animation de chargement. + + Class-level variables: + :cvar COULEUR_MESSAGE_NORMAL (str): La couleur pour les messages normaux en CLI. + :cvar COULEUR_MESSAGE_ERREUR (str): La couleur pour les messages d'erreur en CLI. + """ + COULEUR_MESSAGE_NORMAL = colorama.Fore.WHITE + COULEUR_MESSAGE_ERREUR = colorama.Fore.RED + + def __init__(self): + """ + Initialise un objet pour afficher des informations dans la ligne de commande. + """ + # Normalise les codes couleurs pour fonctionner partout + colorama.init() + # Initialise les variables pour le chargement + self._thread_chargement = None + self._thread_chargement_termine = threading.Event() + self._thread_chargement_erreur = threading.Event() + # Récupère les animations + chemin_racine = Path(__file__).parent.parent.parent.resolve() + chemin_animations = chemin_racine / "assets" / "animations.json" + with open(chemin_animations, "r", encoding="utf-8") as animations: + elements_animations = load(animations) + # Choisis une animation au hasard parmi chaque catégorie d'animation + self._animations_actuelles = { + "chasseur": choice(elements_animations["chasseurs"]), + "fantome": choice(elements_animations["fantomes"]), + "rayon_laser": elements_animations["rayons_laser"] + } + + def reecrire_ligne(self, message: str): + """ + Permet d'écrire des caractères par dessus la dernière ligne dans la + ligne de commande. + + Args: + message (str): Les caractères à afficher. + + Returns: + None + + Raises: + TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères. + """ + # Validation du paramètre + if not isinstance(message, str): + raise TypeError("Le message pour la réécriture doit être une chaîne de caractères.") + # Ecriture du message + sys.stdout.write("\r" + self.COULEUR_MESSAGE_NORMAL + message) + sys.stdout.flush() + + def affiche_message(self, message: str): + """ + Permet d'écrire un message commun dans la ligne de commande avec la bonne + couleur. + + Args: + message (str): Le message à afficher. + + Returns: + None + + Raises: + TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères. + """ + # Validation du paramètre + if not isinstance(message, str): + raise TypeError("Le message doit être une chaîne de caractères.") + # Ecriture du message + print(self.COULEUR_MESSAGE_NORMAL + message, flush=True) + + def affiche_erreur(self, message: str, exception: Exception): + """ + Permet d'écrire un message d'erreur dans la ligne de commande avec la bonne + couleur. + + Args: + message (str): Le message à afficher. + exception (Exception): L'exception à afficher. + + Returns: + None + + Raises: + TypeError: Le paramètre ``message`` n'est pas une chaîne de caractères ou + le paramètre ``exception`` n'est pas une instance de la classe :class:`Exception`. + """ + # Validation des paramètres + if not isinstance(message, str): + raise TypeError("Le message d'erreur doit être une chaîne de caractères.") + if not isinstance(exception, Exception): + raise TypeError("L'exception à afficher doit être une instance de Exception.") + # Ecriture du message + print(self.COULEUR_MESSAGE_ERREUR + f"{message}\n{exception}", flush=True) + + def lance_animation_chargement(self): + """ + Lance une animation de chargement dans la ligne de commande via un thread non bloquant. + Si l'animation de chargement est déjà en cours, cette méthode ne fait rien. + + Returns: + None + """ + # Si le thread est déjà lancé, annulation de l'animation + if self._thread_chargement is None: + # On réinitialise les demandes d'arrêt + self._thread_chargement_termine.clear() + self._thread_chargement_erreur.clear() + # Initialisation du thread pour le chargement + self._thread_chargement = threading.Thread(target=self._animation_chargement, daemon=True) + # Lancement du thread pour le chargement + self._thread_chargement.start() + + def _animation_chargement(self): + """ + Lance l'animation de chargement en boucle jusqu'à la demande d'arrêt via + l'attribut :attr:`_thread_chargement_demande_arret`. + + Returns: + None + """ + # Eléments de l'animation de chargement + chasseur_chargement = self._animations_actuelles["chasseur"][0] + fantome_chargement = self._animations_actuelles["fantome"][0] + signes_rayon_laser = self._animations_actuelles["rayon_laser"] + couleurs = ["\033[91m", "\033[93m", "\033[94m", "\033[95m"] # Rouge, Jaune, Bleu, Magenta + + # Eléments de l'animation de fin de chargement en cas de succès + chasseur_gagne = self._animations_actuelles["chasseur"][2] + fantome_perd = self._animations_actuelles["fantome"][1] + + # Eléments de l'animation de fin de chargement en cas d'erreur + chasseur_perd = self._animations_actuelles["chasseur"][1] + fantome_gagne = self._animations_actuelles["fantome"][2] + + # Variables pour l'animation de chargement + index_boucle = 0 + rayon_laser = "" + + # Début de l'animation (jusqu'à la demande d'arrêt) + while not (self._thread_chargement_termine.is_set() + or self._thread_chargement_erreur.is_set()): + # Arrête d'ajouter des caractères lorsque la chaîne est trop longue + if (index_boucle < 40): + # Récupération de la prochaine couleur + couleur_courante = couleurs[(index_boucle % len(couleurs))] + # Récupération du prochain signe du rayon + signe_courant = signes_rayon_laser[(index_boucle % len(signes_rayon_laser))] + # Ajout du dernier signe avec la nouvelle couleur au rayon + rayon_laser += couleur_courante + signe_courant + # Réactualisation de l'animation de chargement + self.reecrire_ligne(f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}") + index_boucle += 1 + sleep(0.05) + + # Suppression de la ligne de chargement + self.reecrire_ligne("\033[K") + espace_rayon_laser = " " * index_boucle + if (self._thread_chargement_termine.is_set()): + # Message d'animation terminée + self.reecrire_ligne(f"{chasseur_gagne}{espace_rayon_laser}\033[0m{fantome_perd}\n") + self.affiche_message(f"Analyse terminée! We came, we saw, we logged it.") + else: + # Message d'animation erreur + self.reecrire_ligne(f"{chasseur_perd}{espace_rayon_laser}\033[0m{fantome_gagne}\n") + + def stop_animation_chargement(self, erreur: bool = False): + """ + Lance une demande d'arrêt au thread qui gère l'animation de chargement + en cours. Si aucune animation n'est en cours, cette méthode ne fait rien. + + Args: + erreur (bool): Indique si la demande d'arrêt est dûe à une erreur ou non. + + Returns: + None + """ + # Si le thread de chargement existe et est lancé + if self._thread_chargement and self._thread_chargement.is_alive(): + # Lancement de la demade d'arrêt + if not erreur: + self._thread_chargement_termine.set() + else: + self._thread_chargement_erreur.set() + # Attente de l'arrêt depuis le thread principal + self._thread_chargement.join() + self._thread_chargement = None diff --git a/app/main.py b/app/main.py index 4833dc2..e75eced 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,7 @@ """ Point d'entrée de l'application LogBuster ! """ - -import colorama +from cli.afficheur_cli import AfficheurCLI from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException from analyse.analyseur_log_apache import AnalyseurLogApache @@ -13,20 +12,10 @@ def main(): """ Point d'entrée de l'application. """ - colorama.init() - print(colorama.Style.DIM + r""" - .-. .-') .-') .-') _ ('-. _ .-') ,---. - \ ( OO ) ( OO ). ( OO) ) _( OO)( \( -O ) | | - ,--. .-'),-----. ,----. ;-----.\ ,--. ,--. (_)---\_)/ '._(,------.,------. | | - | |.-') ( OO' .-. ' ' .-./-') | .-. | | | | | / _ | |'--...__)| .---'| /`. '| | - | | OO )/ | | | | | |_( O- )| '-' /_) | | | .-') \ :` `. '--. .--'| | | / | || | - | |`-' |\_) | |\| | | | .--, \| .-. `. | |_|( OO ) '..`''.) | | (| '--. | |_.' || .' -(| '---.' \ | | | |(| | '. (_/| | \ | | | | `-' /.-._) \ | | | .--' | . '.'`--' - | | `' '-' ' | '--' | | '--' /(' '-'(_.-' \ / | | | `---.| |\ \ .--. - `------' `-----' `------' `------' `-----' `-----' `--' `------'`--' '--''--' - - """) + afficheur_cli = AfficheurCLI() + afficheur_cli.affiche_message("Who ya gonna call? LogBuster!") try: + afficheur_cli.lance_animation_chargement() # Récupération des arguments parseur_cli = ParseurArgumentsCLI() arguments_cli = parseur_cli.parse_args() @@ -39,17 +28,33 @@ def main(): # Exportation de l'analyse exporteur = Exporteur(arguments_cli.sortie) exporteur.export_vers_json(analyse) - except ArgumentCLIException as ex: - print(f"Erreur dans les arguments fournis !\n {ex}") - except FileNotFoundError as ex: - print(f"Erreur dans la recherche du log Apache !\n{ex}") - except FormatLogApacheInvalideException as ex: - print(f"Erreur dans l'analyse du log Apache !\n{ex}") - except ExportationException as ex: - print(f"Erreur dans l'exportation de l'analyse !\n{ex}") + afficheur_cli.stop_animation_chargement() except Exception as ex: - print(f"Erreur interne !\n{ex}") + gestion_exception(afficheur_cli, ex) + +def gestion_exception(afficheur_cli, exception): + """ + Gère les erreurs qui demandent une fin du programme. + Affiche également un message d'erreur personnalisé en fonction + de l'exception. + Args: + afficheur_cli (AfficheurCLI): L'objet permettant d'intéragir avec la ligne + de commande. + exception (Exception): L'exception qui s'est produite. + + Returns: + None + """ + erreurs = { + ArgumentCLIException: "Erreur dans les arguments fournis !", + FileNotFoundError: "Erreur dans la recherche du log Apache !", + FormatLogApacheInvalideException: "Erreur dans l'analyse du log Apache !", + ExportationException: "Erreur dans l'exportation de l'analyse !" + } + message = erreurs.get(type(exception), "Erreur interne !") + afficheur_cli.stop_animation_chargement(True) + afficheur_cli.affiche_erreur(message, exception) if __name__ == "__main__": main() diff --git a/assets/animations.json b/assets/animations.json new file mode 100644 index 0000000..eb38a6f --- /dev/null +++ b/assets/animations.json @@ -0,0 +1,18 @@ +{ + "chasseurs": [ + ["(҂-_•)⊃═O", "(҂x_x)", "d(•᎑-҂)"], + ["(⌐■_■)⊃═O", "(⌐x_x)", "d(■᎑■⌐)"], + ["(∩•_•)⊃═O", "(∩x_x)", "d(•᎑•∩)"], + ["(ò_ó)⊃═O", "(x_x)", "d(ò᎑ó)"] + ], + + "fantomes": [ + ["ε=(( ꐑº-° )ꐑ", "(( x-x)", "ε=(( ꐑº᎑° )ꐑ"], + ["ε=(¬ ´ཀ` )¬", "( xཀx)", "ε=(¬ ´᎑` )¬"], + ["ε=༼ つ ╹ ╹ ༽つ", "༼ x x ༽", "ε=༼ つ ╹᎑╹ ༽つ"], + ["ε=( >_<)", "( x_x)", "ε=( >᎑<)"], + ["ε=(ง'̀-'́)ง", "('x-x)", "ε=(ง'̀᎑'́)ง"] + ], + + "rayons_laser": ["-", "=", "~", "*", "~", "=", "-"] +} \ No newline at end of file diff --git a/docs/source/modules/cli/afficheur_cli.rst b/docs/source/modules/cli/afficheur_cli.rst new file mode 100644 index 0000000..ff28b66 --- /dev/null +++ b/docs/source/modules/cli/afficheur_cli.rst @@ -0,0 +1,7 @@ +AfficheurCLI +====================== + +.. automodule:: cli.afficheur_cli + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/cli/index_cli.rst b/docs/source/modules/cli/index_cli.rst index 5494b23..3f644e3 100644 --- a/docs/source/modules/cli/index_cli.rst +++ b/docs/source/modules/cli/index_cli.rst @@ -5,3 +5,4 @@ CLI :maxdepth: 4 parseur_arguments_cli.rst + afficheur_cli.rst diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000..7d5cb5a --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,3 @@ +[report] +exclude_lines = + if __name__ == "__main__": \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 550738c..861d5e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ """ import pytest +from cli.afficheur_cli import AfficheurCLI from cli.parseur_arguments_cli import ParseurArgumentsCLI from parse.parseur_log_apache import ParseurLogApache from analyse.analyseur_log_apache import AnalyseurLogApache @@ -47,6 +48,10 @@ # Fixtures générales # ------------------ +@pytest.fixture +def afficheur_cli(): + return AfficheurCLI() + @pytest.fixture def parseur_arguments_cli(): """ diff --git a/tests/test_afficheur_cli.py b/tests/test_afficheur_cli.py new file mode 100644 index 0000000..c68ec51 --- /dev/null +++ b/tests/test_afficheur_cli.py @@ -0,0 +1,211 @@ +""" +Modules des tests unitaires pour l'affichage d'informations dans la CLI. +""" + +import pytest +from time import sleep +from io import StringIO +from cli.afficheur_cli import AfficheurCLI + +# Données +messages = [ + "Un message normal", + "Test\r avec des \ncaractères\t spéciaux", + "Test d'un message très long" * 1000, + "\033[31mTest avec couleur\033[0m", + "🎉 Test avec des unicodes 🎉" +] + +# Tests unitaires + +@pytest.mark.parametrize("message", messages) +def test_afficheur_cli_affiche_message(mocker, afficheur_cli, message): + """ + Vérifie que la méthode affiche_message affiche correctement les messages avec la bonne + couleur dans le terminal. + + Scénarios testés: + - Passage d'un message à la méthode. + + Asserts: + - Le message est le même que celui fourni avec la bonne couleur. + + Args: + mocker (MockerFixture): Fixture qui permet de modifier le stdout. + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + message (str): Message à afficher. + """ + # Remplacement de la sortie standard par un StringIO (nommé sortie_cli) + with mocker.patch("sys.stdout", new_callable=StringIO) as sortie_cli: + afficheur_cli.affiche_message(message) + assert sortie_cli.getvalue() == afficheur_cli.COULEUR_MESSAGE_NORMAL + message + "\n" + +def test_afficheur_cli_exception_affiche_message_type(afficheur_cli): + """ + Vérifie que la méthode affiche_message lève une exception TypeError + lorsque l'argument fourni n'est pas une chaîne de caractères. + + Scénarios testés: + - Passage d'un entier à la méthode. + + Asserts: + - Vérifie que l'exception TypeError est levée. + + Args: + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + """ + with pytest.raises(TypeError): + afficheur_cli.affiche_message(123) + +@pytest.mark.parametrize("message, exception", [ + ("Un message d'erreur !", Exception("Erreur générale !")), + ("Erreur de typage !", TypeError("Une variable str était attendue !")), + ("Erreur dans la recherche !", FileNotFoundError("Fichier non trouvé !")) +]) +def test_afficheur_cli_affiche_erreur(mocker, afficheur_cli, message, exception): + """ + Vérifie que la méthode affiche_erreur affiche correctement les messages d'erreur + dans le terminal avec la couleur adéquate. + + Scénarios testés: + - Affichage d'erreurs avec différents types d'exceptions. + + Asserts: + - Vérifie que le message d'erreur et l'exception sont bien affichés. + + Args: + mocker (MockerFixture): Fixture pour modifier le stdout. + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + message (str): Message d'erreur à afficher. + exception (Exception): Exception associée à l'erreur. + """ + # Remplacement de la sortie standard par un StringIO (nommé sortie_cli) + with mocker.patch("sys.stdout", new_callable=StringIO) as sortie_cli: + afficheur_cli.affiche_erreur(message, exception) + assert sortie_cli.getvalue() == (afficheur_cli.COULEUR_MESSAGE_ERREUR + + message + "\n" + str(exception) + "\n") + +@pytest.mark.parametrize("message, exception", [ + (False, Exception("Erreur générale !")), + ("Erreur de typage !", False) +]) +def test_afficheur_cli_exception_affiche_erreur_type(afficheur_cli, message, exception): + """ + Vérifie que la méthode affiche_erreur lève une exception TypeError + lorsque les arguments ne sont pas du bon type. + + Scénarios testés: + - Passage d'un mauvais type pour le paramètre ``message``. + - Passage d'un mauvais type pour le paramètre ``exception``. + + Asserts: + - Vérifie que l'exception TypeError est levée. + + Args: + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + message (Any): Valeur incorrecte à tester. + exception (Any): Valeur incorrecte à tester. + """ + with pytest.raises(TypeError): + afficheur_cli.affiche_erreur(message, exception) + +@pytest.mark.parametrize("message", messages) +def test_afficheur_cli_reecrire_ligne(mocker, afficheur_cli, message): + """ + Vérifie que la méthode reecrire_ligne affiche correctement un message + sur la même ligne du terminal. + + Scénarios testés: + - Affichage de plusieurs messages. + + Asserts: + - Vérifie que la sortie standard contient le message formaté. + + Args: + mocker (MockerFixture): Fixture pour modifier le stdout. + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + message (str): Message à afficher. + """ + # Remplacement de la sortie standard par un StringIO (nommé sortie_cli) + with mocker.patch("sys.stdout", new_callable=StringIO) as sortie_cli: + afficheur_cli.reecrire_ligne(message) + assert sortie_cli.getvalue() == "\r" + afficheur_cli.COULEUR_MESSAGE_NORMAL + message + +def test_afficheur_cli_exception_reecrire_ligne_type(afficheur_cli): + """ + Vérifie que la méthode reecrire_ligne lève une exception TypeError + lorsqu'un argument de type incorrect est fourni. + + Scénarios testés: + - Passage d'un entier au lieu d'une chaîne de caractères. + + Asserts: + - Vérifie que l'exception TypeError est levée. + + Args: + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + """ + with pytest.raises(TypeError): + afficheur_cli.reecrire_ligne(123) + +def test_afficheur_cli_lance_animation_chargement(mocker, afficheur_cli): + """ + Vérifie que la méthode lance_animation_chargement démarre bien un thread + pour l'animation de chargement. + + Scénarios testés: + - Lancement de l'animation de chargement. + + Asserts: + - Vérifie que le thread est bien initialisé et actif. + + Args: + mocker (MockerFixture): Fixture pour modifier les méthodes internes. + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + """ + mocker.patch.object(afficheur_cli, "_animation_chargement", side_effect=lambda: sleep(10)) + assert afficheur_cli._thread_chargement is None + afficheur_cli.lance_animation_chargement() + assert afficheur_cli._thread_chargement is not None + assert afficheur_cli._thread_chargement.is_alive() + +def test_afficheur_cli_stop_animation_chargement_terminee(afficheur_cli): + """ + Vérifie que la méthode stop_animation_chargement arrête correctement l'animation en + cas de demande d'arrêt suite à une fin de chargement normale. + + Scénarios testés: + - Arrêt normal de l'animation de chargement. + + Asserts: + - Vérifie que le thread est bien arrêté. + - Vérifie que le flag d'arrêt normale est activé. + + Args: + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + """ + afficheur_cli.lance_animation_chargement() + afficheur_cli.stop_animation_chargement() + assert afficheur_cli._thread_chargement is None + assert afficheur_cli._thread_chargement is None + assert afficheur_cli._thread_chargement_termine.is_set() + +def test_afficheur_cli_stop_animation_chargement_erreur(afficheur_cli): + """ + Vérifie que la méthode stop_animation_chargement arrête correctement l'animation en + cas de demande d'arrêt suite à une erreur. + + Scénarios testés: + - Arrêt normal de l'animation de chargement. + + Asserts: + - Vérifie que le thread est bien arrêté. + - Vérifie que le flag d'arrêt d'erreur est activé. + + Args: + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + """ + afficheur_cli.lance_animation_chargement() + afficheur_cli.stop_animation_chargement(True) + assert afficheur_cli._thread_chargement is None + assert afficheur_cli._thread_chargement_erreur.is_set() \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index c2fb6e1..a92fba9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -24,7 +24,7 @@ def test_main_gestion_exception(mocker, exception): - Vérification que les exceptions n'arrête pas le programme. Args: - mocker (any): Une fixture pour simuler des exceptions. + mocker (MockerFixture): Une fixture pour simuler des exceptions. exception (any): L'exception à simuler. """ mocker.patch("main.ParseurArgumentsCLI", side_effect=exception) @@ -40,7 +40,7 @@ def test_main_succes(mocker): d'un déroulement normal. Args: - mocker (any): Une fixture pour simuler des retours pour les classes + mocker (MockerFixture): Une fixture pour simuler des retours pour les classes et méthodes dans main. """ # Mock des classes pour simuler un fonctionnement correct From a2f805ac65d1b903d20022a1235427c80cd6ce66 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:43:32 +0200 Subject: [PATCH 40/47] =?UTF-8?q?Chore:=20Ajout=20du=20fichier=20g=C3=A9n?= =?UTF-8?q?=C3=A9r=C3=A9=20par=20l'application=20dans=20.gitignore=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout du fichier généré par l'application dans le .gitignore afin de faciliter le développement - Modification de certains commentaires erronés --- .gitignore | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index dc69735..9dda353 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ *.pyc __pycache__/ -# Fichiers propres à Python +# Fichiers propres à VisualStudioCode .vscode/ .idea/ @@ -17,4 +17,7 @@ __pycache__/ htmlcov/ # Fichiers de la documentation sphinx -docs/build/ \ No newline at end of file +docs/build/ + +# Fichier de tests durant le développement +analyse-log-apache.json \ No newline at end of file From 54f5d27133608b639c7852e7082c2d570b5ef999 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 5 Apr 2025 12:42:39 +0200 Subject: [PATCH 41/47] =?UTF-8?q?Refactor:=20Mise=20en=20conformit=C3=A9?= =?UTF-8?q?=20avec=20PEP8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .github/workflows/documentation.yaml | 2 +- .github/workflows/tests.yaml | 2 +- app/analyse/analyseur_log_apache.py | 32 ++- app/cli/afficheur_cli.py | 39 ++-- app/cli/parseur_arguments_cli.py | 46 +++- app/donnees/client_informations.py | 19 +- app/donnees/reponse_informations.py | 15 +- app/donnees/requete_informations.py | 21 +- app/export/exporteur.py | 47 ++++- app/main.py | 34 +-- app/parse/entree_log_apache.py | 47 +++++ app/parse/fichier_log_apache.py | 55 +++-- app/parse/parseur_log_apache.py | 198 ++++++++++++++---- .../modules/donnees/client_informations.rst | 1 + .../modules/donnees/reponse_informations.rst | 1 + .../modules/donnees/requete_informations.rst | 2 +- .../modules/parse/entree_log_apache.rst | 8 + .../modules/parse/fichier_log_apache.rst | 1 + docs/source/modules/parse/index_parse.rst | 3 +- tests/conftest.py | 8 +- tests/test_afficheur_cli.py | 17 ++ tests/test_exporteur.py | 23 +- tests/test_fichier_entree_log_apache.py | 86 ++++++++ tests/test_main.py | 37 ++-- tests/test_parseur_arguments_cli.py | 25 +++ tests/test_parseur_log_apache.py | 118 ++++++++++- 26 files changed, 719 insertions(+), 168 deletions(-) create mode 100644 app/parse/entree_log_apache.py create mode 100644 docs/source/modules/parse/entree_log_apache.rst create mode 100644 tests/test_fichier_entree_log_apache.py diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 4501cc5..7979dd1 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -1,4 +1,4 @@ -name: Construction et déploiement de la configuration +name: Documentation - LogBuster on: push: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9497987..e400e16 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -48,5 +48,5 @@ jobs: uses: actions/upload-artifact@v4 with: if-no-files-found: error - name: tests-resultats-python-${{ matrix.python-version }} # Nom de l'artefact + name: rapport-tests-unitaires-python-${{ matrix.python-version }} # Nom de l'artefact path: tests/resultats_pytest # Eléments à sauvegarder diff --git a/app/analyse/analyseur_log_apache.py b/app/analyse/analyseur_log_apache.py index 26e012d..f00552f 100644 --- a/app/analyse/analyseur_log_apache.py +++ b/app/analyse/analyseur_log_apache.py @@ -27,22 +27,27 @@ def __init__(self, fichier_log_apache: FichierLogApache, nombre_par_top: int = 3 les statistiques des classements (tops). Par défaut, sa valeur est égale à ``3``. Raises: - TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance de :class:`FichierLogApache` + TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance + de :class:`FichierLogApache` ou si l'argument ``nombre_par_top`` n'est pas un entier. ValueError: Si l'argument ``nombre_par_top`` est inférieur à ``0``. """ + # Vérification du type des paramètres if not isinstance(fichier_log_apache, FichierLogApache): raise TypeError("La représentation du fichier doit être de type FichierLogApache.") if not isinstance(nombre_par_top, int) or isinstance(nombre_par_top, bool): raise TypeError("Le nombre par top doit être un entier.") + # Vérification de la valeur du paramètre if nombre_par_top < 0: raise ValueError("Le nombre par top doit être supérieur ou égale à 0.") + + # Ajout des données self.fichier = fichier_log_apache self.nombre_par_top = nombre_par_top def _get_repartition_elements(self, - liste_elements: list, - nom_elements: str, + liste_elements: list, + nom_elements: str, mode_top_classement: bool = False) -> list: """ Retourne le top 'n' des éléments qui apparaissent le plus dans la liste. @@ -64,21 +69,25 @@ def _get_repartition_elements(self, :attr:`nombre_par_top` éléments, mais peut en contenir moins s'il y a moins de :attr:`nombre_par_top` éléments distincts. """ + # Vérification du type des paramètres if not isinstance(liste_elements, list): raise TypeError("La liste des éléments doit être une instance du type list.") if not isinstance(nom_elements, str): raise TypeError("Le nom des éléments doit être une chaîne de caractères") if not isinstance(mode_top_classement, bool): - raise TypeError("L'indication de l'activation du mode 'top_classement' doi être un booléen.") - + raise TypeError("L'indication de l'activation du mode 'top_classement' " + "doit être un booléen.") + + # Analyse de la liste total_elements = len(liste_elements) compteur_elements = Counter(liste_elements) - top_elements = compteur_elements.most_common(self.nombre_par_top if mode_top_classement else None) + top_elements = compteur_elements.most_common(self.nombre_par_top + if mode_top_classement else None) return [ {nom_elements: element, "total": total, "taux": total / total_elements * 100} for element, total in top_elements ] - + def get_analyse_complete(self) -> dict: """ Retourne l'analyse complète du fichier de log Apache. @@ -88,7 +97,8 @@ def get_analyse_complete(self) -> dict: - statistiques: - requetes: - top_urls: voir :meth:`get_top_urls` - - repartition_code_statut_http: voir :meth:`get_total_par_code_statut_http` + - repartition_code_statut_http: + voir :meth:`get_total_par_code_statut_http` Returns: dict: L'analyse sous forme d'un dictionnaire. @@ -112,7 +122,7 @@ def get_total_entrees(self) -> int: int: Le nombre total d'entrées. """ return len(self.fichier.entrees) - + def get_top_urls(self) -> list: """ Retourne le top :attr:`nombre_par_top` des urls les plus demandées. @@ -130,7 +140,7 @@ def get_top_urls(self) -> list: "url", True ) - + def get_total_par_code_statut_http(self) -> list: """ 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: return self._get_repartition_elements( [entree.reponse.code_statut_http for entree in self.fichier.entrees], "code" - ) \ No newline at end of file + ) diff --git a/app/cli/afficheur_cli.py b/app/cli/afficheur_cli.py index 5083b13..e16f9d6 100644 --- a/app/cli/afficheur_cli.py +++ b/app/cli/afficheur_cli.py @@ -7,8 +7,9 @@ from json import load from time import sleep from random import choice -import colorama import threading +import colorama + class AfficheurCLI: """ @@ -51,7 +52,7 @@ def __init__(self): "rayon_laser": elements_animations["rayons_laser"] } - def reecrire_ligne(self, message: str): + def reecrire_ligne(self, message: str) -> None: """ Permet d'écrire des caractères par dessus la dernière ligne dans la ligne de commande. @@ -68,11 +69,12 @@ def reecrire_ligne(self, message: str): # Validation du paramètre if not isinstance(message, str): raise TypeError("Le message pour la réécriture doit être une chaîne de caractères.") + # Ecriture du message sys.stdout.write("\r" + self.COULEUR_MESSAGE_NORMAL + message) sys.stdout.flush() - def affiche_message(self, message: str): + def affiche_message(self, message: str) -> None: """ Permet d'écrire un message commun dans la ligne de commande avec la bonne couleur. @@ -89,10 +91,11 @@ def affiche_message(self, message: str): # Validation du paramètre if not isinstance(message, str): raise TypeError("Le message doit être une chaîne de caractères.") + # Ecriture du message print(self.COULEUR_MESSAGE_NORMAL + message, flush=True) - def affiche_erreur(self, message: str, exception: Exception): + def affiche_erreur(self, message: str, exception: Exception) -> None: """ Permet d'écrire un message d'erreur dans la ligne de commande avec la bonne couleur. @@ -113,10 +116,11 @@ def affiche_erreur(self, message: str, exception: Exception): raise TypeError("Le message d'erreur doit être une chaîne de caractères.") if not isinstance(exception, Exception): raise TypeError("L'exception à afficher doit être une instance de Exception.") + # Ecriture du message print(self.COULEUR_MESSAGE_ERREUR + f"{message}\n{exception}", flush=True) - def lance_animation_chargement(self): + def lance_animation_chargement(self) -> None: """ Lance une animation de chargement dans la ligne de commande via un thread non bloquant. 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): self._thread_chargement_termine.clear() self._thread_chargement_erreur.clear() # Initialisation du thread pour le chargement - self._thread_chargement = threading.Thread(target=self._animation_chargement, daemon=True) + self._thread_chargement = threading.Thread(target=self._animation_chargement, + daemon=True) # Lancement du thread pour le chargement self._thread_chargement.start() - - def _animation_chargement(self): + + def _animation_chargement(self) -> None: """ Lance l'animation de chargement en boucle jusqu'à la demande d'arrêt via l'attribut :attr:`_thread_chargement_demande_arret`. @@ -147,7 +152,7 @@ def _animation_chargement(self): fantome_chargement = self._animations_actuelles["fantome"][0] signes_rayon_laser = self._animations_actuelles["rayon_laser"] couleurs = ["\033[91m", "\033[93m", "\033[94m", "\033[95m"] # Rouge, Jaune, Bleu, Magenta - + # Eléments de l'animation de fin de chargement en cas de succès chasseur_gagne = self._animations_actuelles["chasseur"][2] fantome_perd = self._animations_actuelles["fantome"][1] @@ -164,7 +169,7 @@ def _animation_chargement(self): while not (self._thread_chargement_termine.is_set() or self._thread_chargement_erreur.is_set()): # Arrête d'ajouter des caractères lorsque la chaîne est trop longue - if (index_boucle < 40): + if index_boucle < 40: # Récupération de la prochaine couleur couleur_courante = couleurs[(index_boucle % len(couleurs))] # Récupération du prochain signe du rayon @@ -172,22 +177,24 @@ def _animation_chargement(self): # Ajout du dernier signe avec la nouvelle couleur au rayon rayon_laser += couleur_courante + signe_courant # Réactualisation de l'animation de chargement - self.reecrire_ligne(f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}") + self.reecrire_ligne( + f"{chasseur_chargement}{rayon_laser}\033[0m{fantome_chargement}" + ) index_boucle += 1 sleep(0.05) # Suppression de la ligne de chargement self.reecrire_ligne("\033[K") espace_rayon_laser = " " * index_boucle - if (self._thread_chargement_termine.is_set()): + if self._thread_chargement_termine.is_set(): # Message d'animation terminée self.reecrire_ligne(f"{chasseur_gagne}{espace_rayon_laser}\033[0m{fantome_perd}\n") - self.affiche_message(f"Analyse terminée! We came, we saw, we logged it.") + self.affiche_message("Analyse terminée! We came, we saw, we logged it.") else: # Message d'animation erreur self.reecrire_ligne(f"{chasseur_perd}{espace_rayon_laser}\033[0m{fantome_gagne}\n") - def stop_animation_chargement(self, erreur: bool = False): + def stop_animation_chargement(self, erreur: bool = False) -> None: """ Lance une demande d'arrêt au thread qui gère l'animation de chargement 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): Returns: None """ + # Vérification du type du paramètre + if not isinstance(erreur, bool): + raise TypeError("L'indication d'une erreur doit être un booléan.") + # Si le thread de chargement existe et est lancé if self._thread_chargement and self._thread_chargement.is_alive(): # Lancement de la demade d'arrêt diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py index 030f3db..795e0f2 100644 --- a/app/cli/parseur_arguments_cli.py +++ b/app/cli/parseur_arguments_cli.py @@ -2,8 +2,10 @@ Module pour analyser les arguments passés en ligne de commande. """ -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from re import match +from typing import Optional + class ParseurArgumentsCLI(ArgumentParser): """ @@ -12,14 +14,20 @@ class ParseurArgumentsCLI(ArgumentParser): """ def __init__(self): + """ + Initialise uparseur pour analyser les arguments passés en ligne de commande. + """ super().__init__( description="LogBuster, l'analyseur de log Apache.", allow_abbrev=False, ) self.__set_arguments() - def __set_arguments(self): + def __set_arguments(self) -> None: """ Définit les arguments attendus par l'application. + + Returns: + None """ # -- Argument obligatoire -- self.add_argument( @@ -37,10 +45,32 @@ def __set_arguments(self): "nom 'analyse-log-apache.json' dans le repertoire courant sera crée.", ) - def parse_args(self, args=None, namespace=None): + def parse_args(self, + args: Optional[list] = None, + namespace: Optional[Namespace] = None) -> Namespace: """ - Analyse, vérifie et retourne les arguments fournis en ligne de commande. + Récupère les arguments passés en ligne de commande puis vérifie + que leur format est conforme à ceux attendus. + + Args: + args (Optional[list]): Liste des arguments passés en paramètre. + Si ``None``, les arguments de la ligne de commande sont utilisés. + namespace (Optional[Namespace]): Un espace de noms (namespace) + pour stocker les résultats. Si ``None``, un nouvel espace de noms est créé. + + Returns: + Namespace: L'objet contenant les arguments analysés et leurs valeurs. + + Raises: + ArgumentCLIException: Si une erreur se produit lors du parsing des arguments + (par exemple, si un argument inconnu est fourni ou si son format est invalide). """ + # Vérification du type des paramètres + if args is not None and not isinstance(args, list): + raise TypeError("Les arguments doivent soit être None, soit être dans une liste.") + if namespace is not None and not isinstance(args, Namespace): + raise TypeError("L'espace de noms doit soit être None, soit être un objet Namespace.") + # Analyse des arguments try: arguments_parses = super().parse_args(args, namespace) @@ -49,29 +79,33 @@ def parse_args(self, args=None, namespace=None): # Vérification syntaxique des arguments regex_chemin = r"^[a-zA-Z0-9:_\\\-.\/]+$" + if not match(regex_chemin, arguments_parses.chemin_log): raise ArgumentCLIException( "Le chemin du fichier log doit uniquement contenir les caractères autorisés. " "Les caractères autorisés sont les minuscules, majuscules, chiffres ou les " "caractères spéciaux suivants: _, \\, -, /." ) + if not match(regex_chemin, arguments_parses.sortie): raise ArgumentCLIException( "Le chemin du fichier de sortie doit uniquement contenir les caractères " "autorisés. Les caractères autorisés sont les minuscules, majuscules, " "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 class ArgumentCLIException(Exception): """ - Représente une erreur liée l'analyse d'un argument en ligne de commande. + Représente une erreur lorsque un argument passé en ligne de commande + est inconnu ou que son format est invalide. """ def __init__(self, *args): diff --git a/app/donnees/client_informations.py b/app/donnees/client_informations.py index b524f02..4834de4 100644 --- a/app/donnees/client_informations.py +++ b/app/donnees/client_informations.py @@ -14,7 +14,7 @@ class ClientInformations: Cette classe regroupe les données extraites d'une entrée de log, qui concernent le client ayant effectué la requête au serveur Apache. - Args: + Attributes: adresse_ip (str): L'adresse IP du client. identifiant_rfc (Optional[str]): L'identifiant RFC du client. Peut être None si non fournie. @@ -22,9 +22,6 @@ class ClientInformations: Peut être None si non fournie. agent_utilisateur (Optional[str]): L'agent utilisateur (User-Agent). Peut être None si non fournie. - - Raises: - TypeError: Si les attributs ne sont pas de type `str` ou `None`. """ adresse_ip: str identifiant_rfc: Optional[str] @@ -32,15 +29,21 @@ class ClientInformations: agent_utilisateur: Optional[str] def __post_init__(self): + """ + Vérifie le bon type des données de cette classe lors de l'initialisation de l'instance. + + Raises: + TypeError: Une donnée n'est pas du bon type. + """ # Validation de l'adresse IP if not isinstance(self.adresse_ip, str): raise TypeError("L'adresse IP est obligatoire et doit être une chaîne de caractères.") # Validation de l'identifiant RFC - if self.identifiant_rfc != None and not isinstance(self.identifiant_rfc, str): + if self.identifiant_rfc is not None and not isinstance(self.identifiant_rfc, str): raise TypeError("L'identifiant RFC doit être une chaîne de caractères ou None.") # Validation du nom d'utilisateur - if self.nom_utilisateur != None and not isinstance(self.nom_utilisateur, str): + if self.nom_utilisateur is not None and not isinstance(self.nom_utilisateur, str): raise TypeError("Le nom d'utilisateur doit être une chaîne de caractères ou None.") # Validation de l'agent utilisateur - if self.agent_utilisateur != None and not isinstance(self.agent_utilisateur, str): - raise TypeError("L'agent utilisateur doit être une chaîne de caractères ou None.") \ No newline at end of file + if self.agent_utilisateur is not None and not isinstance(self.agent_utilisateur, str): + raise TypeError("L'agent utilisateur doit être une chaîne de caractères ou None.") diff --git a/app/donnees/reponse_informations.py b/app/donnees/reponse_informations.py index 8fcd125..b1c6fff 100644 --- a/app/donnees/reponse_informations.py +++ b/app/donnees/reponse_informations.py @@ -15,25 +15,28 @@ class ReponseInformations: qui concernent les informations techniques sur la réponse émise par le serveur Apache au client. - Args: + Attributes: code_statut_http (int): Le code de statut HTTP. taille_octets (Optional[int]): La taille de la réponse en octets. Peut être None si non fournie. - - Raises: - TypeError: Si les attributs ne sont pas du type int. """ code_statut_http: int taille_octets: Optional[int] def __post_init__(self): + """ + Vérifie le bon type des données de cette classe lors de l'initialisation de l'instance. + + Raises: + TypeError: Une donnée n'est pas du bon type. + """ # Vérification du code de statut HTTP - if (not isinstance(self.code_statut_http, int) + if (not isinstance(self.code_statut_http, int) or isinstance(self.code_statut_http, bool)): raise TypeError("Le code de statut HTTP doit être un entier.") # Vérification de la taille de la réponse (en octets) - if (self.taille_octets != None + if (self.taille_octets is not None and not isinstance(self.taille_octets, int) or isinstance(self.taille_octets, bool)): raise TypeError("La taille en octets doit être un entier ou None.") diff --git a/app/donnees/requete_informations.py b/app/donnees/requete_informations.py index 5e4b47b..2206227 100644 --- a/app/donnees/requete_informations.py +++ b/app/donnees/requete_informations.py @@ -15,7 +15,7 @@ class RequeteInformations: qui concernent les informations techniques sur la requête émise au serveur Apache. - Args: + Attributes: horodatage (datetime): L'horodatage de la requête. methode_http (Optional[str]): La méthode HTTP utilisée. Peut être None si non fournie. @@ -25,9 +25,6 @@ class RequeteInformations: Peut être None si non fournie. ancienne_url (Optional[str]): L'URL de provenance (referrer). Peut être None si non fournie. - - Raises: - TypeError: Si les attributs ne sont pas du type attendu ou None. """ horodatage: datetime methode_http: Optional[str] @@ -36,18 +33,24 @@ class RequeteInformations: ancienne_url: Optional[str] def __post_init__(self): + """ + Vérifie le bon type des données de cette classe lors de l'initialisation de l'instance. + + Raises: + TypeError: Une donnée n'est pas du bon type. + """ # Vérification de l'horodatage if not isinstance(self.horodatage, datetime): raise TypeError("L'horodatage doit être de type datetime.") # Vérification de la méthode HTTP - if self.methode_http != None and not isinstance(self.methode_http, str): + if self.methode_http is not None and not isinstance(self.methode_http, str): raise TypeError("La méthode HTTP doit être une chaine de caractère ou None.") # Vérification de la ressource demandée - if self.url != None and not isinstance(self.url, str): + if self.url is not None and not isinstance(self.url, str): raise TypeError("L'URL doit être une chaine de caractère ou None.") # Vérification du protocole HTTP - if self.protocole_http != None and not isinstance(self.protocole_http, str): + if self.protocole_http is not None and not isinstance(self.protocole_http, str): raise TypeError("Le protocole HTTP doit être une chaine de caractère ou None.") # Vérification de l'ancienne URL - if self.ancienne_url != None and not isinstance(self.ancienne_url, str): - raise TypeError("L'ancienne URL doit être une chaine de caractère ou None.") \ No newline at end of file + if self.ancienne_url is not None and not isinstance(self.ancienne_url, str): + raise TypeError("L'ancienne URL doit être une chaine de caractère ou None.") diff --git a/app/export/exporteur.py b/app/export/exporteur.py index 2518e17..bbcc3c7 100644 --- a/app/export/exporteur.py +++ b/app/export/exporteur.py @@ -26,20 +26,43 @@ def __init__(self, chemin_sortie: str): Raises: TypeError: Le chemin de sortie n'est pas une chaîne de caractère. - ExportationException: Exportation impossible à cause de - l'emplacement invalide du fichier de sortie. + ExportationDossierParentException: Exportation impossible à cause de + l'inexistance du dossier parent du fichier d'exportation. """ + # 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ère.") + # Vérification du chemin d'exportation + self.verification_exportation_possible(chemin_sortie) + # Ajout du chemin d'exportation + self._chemin_sortie = chemin_sortie + + def verification_exportation_possible(self, chemin_sortie: 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. + + Args: + chemin_sortie (str): Le chemin du fichier d'exportation. + + Returns: + None + + Raises: + ExportationDossierParentException: Le dossier parent du fichier n'existe pas. + """ + # 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 ExportationException(f"Impossible d'exporter vers le " + raise ExportationDossierParentException(f"Impossible d'exporter vers le " f"fichier {chemin_sortie}, son dossier parent " f"{dossier_parent} n'existe pas.") - self._chemin_sortie = chemin_sortie - def export_vers_json(self, donnees: dict): + def export_vers_json(self, donnees: dict) -> None: """ Export le dictionnaire fourni vers le :attr:`chemin de sortie`. @@ -53,12 +76,13 @@ def export_vers_json(self, donnees: dict): TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire. ExportationException: Une erreur lors de l'écriture dans le fichier JSON. """ + # 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.") - + # Exportation try: - with open(self._chemin_sortie, 'w') as fichier: + with open(self._chemin_sortie, 'w', encoding="utf-8") as fichier: dump(donnees, fichier, indent=4) except Exception as ex: raise ExportationException(str(ex)) from ex @@ -67,6 +91,9 @@ class ExportationException(Exception): """ Représente une erreur lors de l'exportation de données. """ - - def __init__(self, *args): - super().__init__(*args) + +class ExportationDossierParentException(ExportationException): + """ + Représente une erreur lorsque une exportation est impossible + lorsque le dossier parent du fichier d'exportation n'existe pas. + """ diff --git a/app/main.py b/app/main.py index e75eced..55f5911 100644 --- a/app/main.py +++ b/app/main.py @@ -3,22 +3,26 @@ """ from cli.afficheur_cli import AfficheurCLI from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException -from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException +from parse.parseur_log_apache import ParseurLogApache, ParsageLogApacheException from analyse.analyseur_log_apache import AnalyseurLogApache from export.exporteur import Exporteur, ExportationException -def main(): +def main() -> None: """ Point d'entrée de l'application. + + Returns: + None """ afficheur_cli = AfficheurCLI() afficheur_cli.affiche_message("Who ya gonna call? LogBuster!") try: - afficheur_cli.lance_animation_chargement() # Récupération des arguments parseur_cli = ParseurArgumentsCLI() arguments_cli = parseur_cli.parse_args() + # Lance l'animation de chargement + afficheur_cli.lance_animation_chargement() # Analyse syntaxique du fichier log parseur_log = ParseurLogApache(arguments_cli.chemin_log) fichier_log = parseur_log.parse_fichier() @@ -28,31 +32,31 @@ def main(): # Exportation de l'analyse exporteur = Exporteur(arguments_cli.sortie) exporteur.export_vers_json(analyse) + # Termine l'animation de chargement afficheur_cli.stop_animation_chargement() - except Exception as ex: - gestion_exception(afficheur_cli, ex) + except ArgumentCLIException as ex: + gestion_exception(afficheur_cli, "Erreur dans les arguments fournis !", ex) + except ParsageLogApacheException as ex: + gestion_exception(afficheur_cli, "Erreur dans l'analyse du log Apache !", ex) + except ExportationException as ex: + gestion_exception(afficheur_cli, "Erreur dans l'exportation de l'analyse !", ex) + except (ValueError, TypeError) as ex: + gestion_exception(afficheur_cli, "Erreur interne !", ex) -def gestion_exception(afficheur_cli, exception): +def gestion_exception(afficheur_cli: AfficheurCLI, message: str, exception: Exception) -> None: """ Gère les erreurs qui demandent une fin du programme. - Affiche également un message d'erreur personnalisé en fonction - de l'exception. + Affiche un message d'erreur personnalisé ainsi que les détails de l'exception. Args: afficheur_cli (AfficheurCLI): L'objet permettant d'intéragir avec la ligne de commande. + message (str): Message principal à afficher. exception (Exception): L'exception qui s'est produite. Returns: None """ - erreurs = { - ArgumentCLIException: "Erreur dans les arguments fournis !", - FileNotFoundError: "Erreur dans la recherche du log Apache !", - FormatLogApacheInvalideException: "Erreur dans l'analyse du log Apache !", - ExportationException: "Erreur dans l'exportation de l'analyse !" - } - message = erreurs.get(type(exception), "Erreur interne !") afficheur_cli.stop_animation_chargement(True) afficheur_cli.affiche_erreur(message, exception) diff --git a/app/parse/entree_log_apache.py b/app/parse/entree_log_apache.py new file mode 100644 index 0000000..3a68075 --- /dev/null +++ b/app/parse/entree_log_apache.py @@ -0,0 +1,47 @@ +""" +Module qui contient la classe pour représenter une entrée log Apache. +""" + +from dataclasses import dataclass +from donnees.client_informations import ClientInformations +from donnees.requete_informations import RequeteInformations +from donnees.reponse_informations import ReponseInformations + + +@dataclass +class EntreeLogApache: + """ + Représente une entrée dans un fichier de log Apache. + + Attributes: + client (ClientInformations): Les informations du client. + requete (RequeteInformations): Les informations de la requête. + reponse (ReponseInformations): Les informations de la réponse. + """ + client: ClientInformations + requete: RequeteInformations + reponse: ReponseInformations + + def __post_init__(self): + """ + Vérifie le bon type des données de cette classe lors de l'initialisation de l'instance. + + Raises: + TypeError: Une donnée n'est pas du bon type. + """ + # Validation des informations + if not isinstance(self.client, ClientInformations): + raise TypeError( + "Les informations du client dans une entrée doivent être" + "regroupées au sein d'un objet ClientInformations." + ) + if not isinstance(self.requete, RequeteInformations): + raise TypeError( + "Les informations de la requête dans une entrée doivent être" + "regroupées au sein d'un objet RequeteInformations." + ) + if not isinstance(self.reponse, ReponseInformations): + raise TypeError( + "Les informations de la réponse dans une entrée doivent être" + "regroupées au sein d'un objet ReponseInformations." + ) diff --git a/app/parse/fichier_log_apache.py b/app/parse/fichier_log_apache.py index 49b87c8..0adc2e7 100644 --- a/app/parse/fichier_log_apache.py +++ b/app/parse/fichier_log_apache.py @@ -1,38 +1,53 @@ """ -Module qui contient les classes pour représenter un fichier log Apache. +Module qui contient la classe pour représenter un fichier log Apache. """ +from dataclasses import dataclass, field +from parse.entree_log_apache import EntreeLogApache + +@dataclass class FichierLogApache: """ Représente un fichier de log Apache. + + Attributes: + chemin (str): Le chemin du fichier. + entrees (list): La liste des entrées du fichier. """ + chemin: str + entrees: list = field(default_factory=list) - def __init__(self, chemin): - self.chemin = chemin - self.entrees = [] + def __post_init__(self): + """ + Vérifie le bon type des données de cette classe lors de l'initialisation de l'instance. - def ajoute_entree(self, entree): + Raises: + TypeError: Une donnée n'est pas du bon type. + """ + # Validation du chemin + if not isinstance (self.chemin, str): + raise TypeError("Le chemin du fichier doit être une chaîne de caractère.") + if not isinstance (self.entrees, list): + raise TypeError("La liste des entrées doit être une liste.") + + def ajoute_entree(self, entree: EntreeLogApache) -> None: """ Ajoute une entrée à la liste des entrées du fichier. + Args: entree (EntreeLogApache): L'entrée à ajouter. + Returns: None - """ - self.entrees.append(entree) - + Raises: + TypeError: L'entrée ``entree`` n'est pas un objet :class:`EntreeLogApache`. + """ + # Vérification du type du paramètre + if not isinstance (entree, EntreeLogApache): + raise TypeError("Les informations de l'entrée doivent être dans un objet" + "EntreeLogApache.") -class EntreeLogApache: - """ - Représente une entrée dans un fichier de log Apache. - """ - def __init__(self, - informations_client, - informations_requete, - informations_reponse - ): - self.client = informations_client - self.requete = informations_requete - self.reponse = informations_reponse \ No newline at end of file + # Récupération de l'entrée + self.entrees.append(entree) diff --git a/app/parse/parseur_log_apache.py b/app/parse/parseur_log_apache.py index a296e80..f1fa9e8 100644 --- a/app/parse/parseur_log_apache.py +++ b/app/parse/parseur_log_apache.py @@ -5,7 +5,9 @@ import os from re import match from datetime import datetime -from parse.fichier_log_apache import FichierLogApache, EntreeLogApache +from typing import Optional +from parse.fichier_log_apache import FichierLogApache +from parse.entree_log_apache import EntreeLogApache from donnees.client_informations import ClientInformations from donnees.requete_informations import RequeteInformations from donnees.reponse_informations import ReponseInformations @@ -15,11 +17,12 @@ class ParseurLogApache(): """ Représente un parseur pour faire une analyse synthaxique d'un fichier log Apache. - Attributes: - PATTERN_ENTREE_LOG_APACHE (str): Le pattern regex d'une entrée dans un log Apache. + + Class-level variables: + :cvar PATTERN_ENTREE_LOG_APACHE (str): Le pattern regex d'une entrée dans un log Apache. """ - PATTERN_ENTREE_LOG_APACHE = ( + PATTERN_ENTREE_LOG_APACHE: str = ( r'(?P\S+) (?P\S+) (?P\S+)' r' (\[(?P\d{2}\/\w{3}\/\d{4}:\d{1,2}:\d{1,2}:\d{1,2} \+\d{4})\]|-)' r' "((?P\S+) (?P\S+) (?P\S+)|-)"' @@ -31,28 +34,42 @@ def __init__(self, chemin_log): """ Initialise un nouveau parseur de fichier log Apache et vérifie que le fichier passé en paramètre existe. + Args: chemin_log (str): Le chemin du fichier à analyser. + Raises: - FileNotFoundError: Si le fichier à analyser est introuvable. + TypeError: Le chemin ``chemin_log`` n'est pas de type ``str``. + FichierLogApacheIntrouvableException: Si le fichier à analyser est introuvable. """ + # Vérification du type du paramètre + if not isinstance(chemin_log, str): + raise TypeError("Le chemin du log doit être une chaîne de caractères.") + # Vérification du chemin if not os.path.isfile(chemin_log): - raise FileNotFoundError(f"Le fichier {chemin_log} est introuvable.") + raise FichierLogApacheIntrouvableException(f"Le fichier {chemin_log} est introuvable.") + # Ajout du chemin self.chemin_log = chemin_log - - def parse_fichier(self): + + def parse_fichier(self) -> FichierLogApache: """ Effectue une analyse syntaxique du fichier de log Apache puis retourne une représentation du fichier avec les informations trouvées. + Returns: log_analyse (FichierLogApache): Représentation du fichier. + Raises: FormatLogApacheInvalideException: Format du fichier log invalide. """ + # Initialisation de la représentation du fichier log_analyse = FichierLogApache(self.chemin_log) - with open(self.chemin_log, "r") as log: + # Ouverture du log + with open(self.chemin_log, "r", encoding="utf-8") as log: + # Parcours des entrées du log for numero_ligne, ligne in enumerate(log, start=1): try: + # Parsage de l'entrée entree = self.parse_entree(ligne) log_analyse.ajoute_entree(entree) except FormatLogApacheInvalideException as ex: @@ -63,18 +80,26 @@ def parse_fichier(self): return log_analyse - def parse_entree(self, entree): + def parse_entree(self, entree: str) -> EntreeLogApache: """ Effectue une analyse syntaxique d'une entrée dans un fichier de log Apache puis retourne une représentation de l'entrée avec les informations trouvées. + Args: entree (str): Entrée à analyser. + Returns: entree_analysee (EntreeLogApache): Représentation de l'entrée. + Raises: + TypeError: L'entrée ``entree`` n'est pas de type :class:`EntreeLogApache` FormatLogApacheInvalideException: Format de l'entrée du fichier log invalide. """ + # Vérification du type du paramètre + if not isinstance(entree, str): + raise TypeError("L'entrée doit être représentée sous forme de chaîne de caractères.") + # Analyse de l'entrée analyse = match(self.PATTERN_ENTREE_LOG_APACHE, entree) if not analyse: @@ -84,67 +109,158 @@ def parse_entree(self, entree): resultat_analyse = analyse.groupdict() # Récupération des informations liées au client - adresse_ip = self.get_information_entree(resultat_analyse, "ip") + informations_client = self._extraire_informations_client(resultat_analyse) + + # Récupération des informations liées à la requête + informations_requete = self._extraire_informations_requete(resultat_analyse) + + # Récupération des informations liées à la réponse + informations_reponse = self._extraire_informations_reponse(resultat_analyse) + + # Retour des informations regroupées dans l'objet EntreeLogApache + return EntreeLogApache( + informations_client, informations_requete, informations_reponse + ) + + def _extraire_informations_client(self, analyse_regex: dict) -> ClientInformations: + """ + Extrait les informations liées au client depuis l'analyse regex d'une entrée. + + Args: + analyse_regex (dict): Analyse regex d'une entrée. + + Returns: + ClientInformations: Les informations du client. + + Raises: + TypeError: L'analyse ``analyse_regex`` n'est pas un ``dict``. + FormatLogApacheInvalideException: L'adresse IP n'est pas présente dans l'analyse. + """ + # Vérification du type du paramètre + if not isinstance(analyse_regex, dict): + raise TypeError("L'analyse du regex doit être un dictionnaire.") + + # Adresse IP + adresse_ip = self.get_information_entree(analyse_regex, "ip") if adresse_ip is None: raise FormatLogApacheInvalideException("L'adresse IP est obligatoire.") - identifiant_rfc = self.get_information_entree(resultat_analyse, "rfc") - utilisateur = self.get_information_entree(resultat_analyse, "utilisateur") - agent_utilisateur = self.get_information_entree(resultat_analyse, "agent_utilisateur") + # Identifiant RFC + identifiant_rfc = self.get_information_entree(analyse_regex, "rfc") + # Nom de l'utilisateur + utilisateur = self.get_information_entree(analyse_regex, "utilisateur") + # User-Agent + agent_utilisateur = self.get_information_entree(analyse_regex, "agent_utilisateur") - informations_client = ClientInformations( + return ClientInformations( adresse_ip, identifiant_rfc, utilisateur, agent_utilisateur ) - # Récupération des informations liées à la requête - horodatage = self.get_information_entree(resultat_analyse, "horodatage") + def _extraire_informations_requete(self, analyse_regex: dict) -> RequeteInformations: + """ + Extrait les informations liées à la requête depuis l'analyse regex d'une entrée. + + Args: + analyse_regex (dict): Analyse regex d'une entrée. + + Returns: + RequeteInformations: Les informations de la requête. + + Raises: + TypeError: L'analyse ``analyse_regex`` n'est pas un ``dict``. + FormatLogApacheInvalideException: L'horodatage n'est pas présente dans l'analyse. + """ + # Vérification du type du paramètre + if not isinstance(analyse_regex, dict): + raise TypeError("L'analyse du regex doit être un dictionnaire.") + + # Horodatage + horodatage = self.get_information_entree(analyse_regex, "horodatage") if horodatage: horodatage = datetime.strptime(horodatage, "%d/%b/%Y:%H:%M:%S %z") - if horodatage is None: raise FormatLogApacheInvalideException("L'horodatage est obligatoire.") + # Méthode HTTP + methode_http = self.get_information_entree(analyse_regex, "methode") + # URL de la ressource + url = self.get_information_entree(analyse_regex, "url") + # Protocole HTTP + protocole_http = self.get_information_entree(analyse_regex, "protocole") + # URL de la précédente ressource demandée + ancienne_url = self.get_information_entree(analyse_regex, "ancienne_url") - methode_http = self.get_information_entree(resultat_analyse, "methode") - url = self.get_information_entree(resultat_analyse, "url") - protocole_http = self.get_information_entree(resultat_analyse, "protocole") - ancienne_url = self.get_information_entree(resultat_analyse, "ancienne_url") - - informations_requete = RequeteInformations( + return RequeteInformations( horodatage, methode_http, url, protocole_http, ancienne_url ) - # Récupération des informations liées à la réponse - code_statut = self.get_information_entree(resultat_analyse, "code_status") - code_statut = int(code_statut) + def _extraire_informations_reponse(self, analyse_regex: dict) -> ReponseInformations: + """ + Extrait les informations liées à la réponse depuis l'analyse regex d'une entrée. - taille_octets = self.get_information_entree(resultat_analyse, "taille_octets") + Args: + analyse_regex (dict): Analyse regex d'une entrée. + + Returns: + TypeError: L'analyse ``analyse_regex`` n'est pas un ``dict``. + ReponseInformations: Les informations de la réponse. + """ + # Vérification du type du paramètre + if not isinstance(analyse_regex, dict): + raise TypeError("L'analyse du regex doit être un dictionnaire.") + + # Code de statut + code_statut = self.get_information_entree(analyse_regex, "code_status") + code_statut = int(code_statut) + # Taille de la réponse + taille_octets = self.get_information_entree(analyse_regex, "taille_octets") if taille_octets: taille_octets = int(taille_octets) - informations_reponse = ReponseInformations( + return ReponseInformations( code_statut, taille_octets ) - # Retour des informations regroupées dans l'objet EntreeLogApache - return EntreeLogApache( - informations_client, informations_requete, informations_reponse - ) - - - def get_information_entree(self, analyse_regex, nom_information): + def get_information_entree(self, analyse_regex: dict, nom_information: str) -> Optional[str]: """ Retourne la valeur de l'information dans l'analyse si elle possède une valeur ou None si elle ne possède pas de valeur (égale à - ou vide). + Args: analyse_regex (Match[str]): Résultat du regex de l'analyse. nom_information (str): Nom de l'information souhaitée. + Returns: - Union[str, None]: La valeur sous forme de chaîne de caractère ou None si + Optional[str]: La valeur sous forme de chaîne de caractère ou None si aucune valeur n'a été trouvée. + + Raises: + TypeError: Un paramètre n'a pas le bon type. """ - valeur = analyse_regex.get(nom_information) - return valeur if valeur != "" and valeur != "-" else None + # Vérification du type des paramètres + if not isinstance(analyse_regex, dict): + raise TypeError("L'analyse du regex doit être un dictionnaire.") + if not isinstance(nom_information, str): + raise TypeError("Le nom de l'information doit être une chaîne de caractères.") -class FormatLogApacheInvalideException(Exception): + # Récupération de l'information + valeur = analyse_regex.get(nom_information) + return valeur if valeur not in ("", "-") else None +class ParsageLogApacheException(Exception): + """ + Exception représentant une erreur lors du parsage du fichier + de log Apache. + """ def __init__(self, *args): - super().__init__(*args) \ No newline at end of file + super().__init__(*args) + +class FichierLogApacheIntrouvableException(ParsageLogApacheException): + """ + Exception représentant une erreur lorsque le fichier de log Apache + est introuvable. + """ + +class FormatLogApacheInvalideException(ParsageLogApacheException): + """ + Exception représentant une erreur dans le format du fichier + de log Apache fourni. + """ diff --git a/docs/source/modules/donnees/client_informations.rst b/docs/source/modules/donnees/client_informations.rst index 73a48e5..3671f3e 100644 --- a/docs/source/modules/donnees/client_informations.rst +++ b/docs/source/modules/donnees/client_informations.rst @@ -5,3 +5,4 @@ ClientInformations :members: :show-inheritance: :undoc-members: + :exclude-members: adresse_ip, identifiant_rfc, nom_utilisateur, agent_utilisateur diff --git a/docs/source/modules/donnees/reponse_informations.rst b/docs/source/modules/donnees/reponse_informations.rst index e45f565..e94a60f 100644 --- a/docs/source/modules/donnees/reponse_informations.rst +++ b/docs/source/modules/donnees/reponse_informations.rst @@ -5,3 +5,4 @@ ReponseInformations :members: :show-inheritance: :undoc-members: + :exclude-members: code_statut_http, taille_octets diff --git a/docs/source/modules/donnees/requete_informations.rst b/docs/source/modules/donnees/requete_informations.rst index 4662f22..d504b3e 100644 --- a/docs/source/modules/donnees/requete_informations.rst +++ b/docs/source/modules/donnees/requete_informations.rst @@ -5,4 +5,4 @@ RequeteInformations :members: :show-inheritance: :undoc-members: - :exclude-members: dataclass + :exclude-members: horodatage, methode_http, url, protocole_http, ancienne_url diff --git a/docs/source/modules/parse/entree_log_apache.rst b/docs/source/modules/parse/entree_log_apache.rst new file mode 100644 index 0000000..57699a2 --- /dev/null +++ b/docs/source/modules/parse/entree_log_apache.rst @@ -0,0 +1,8 @@ +EntreeLogApache +====================== + +.. automodule:: parse.entree_log_apache + :members: + :show-inheritance: + :undoc-members: + :exclude-members: client, requete, reponse diff --git a/docs/source/modules/parse/fichier_log_apache.rst b/docs/source/modules/parse/fichier_log_apache.rst index e758d38..285677a 100644 --- a/docs/source/modules/parse/fichier_log_apache.rst +++ b/docs/source/modules/parse/fichier_log_apache.rst @@ -5,3 +5,4 @@ FichierLogApache :members: :show-inheritance: :undoc-members: + :exclude-members: chemin, entrees diff --git a/docs/source/modules/parse/index_parse.rst b/docs/source/modules/parse/index_parse.rst index 6fe508c..13c7002 100644 --- a/docs/source/modules/parse/index_parse.rst +++ b/docs/source/modules/parse/index_parse.rst @@ -5,4 +5,5 @@ Parse :maxdepth: 4 parseur_log_apache.rst - fichier_log_apache.rst \ No newline at end of file + fichier_log_apache.rst + entree_log_apache.rst diff --git a/tests/conftest.py b/tests/conftest.py index 861d5e2..9dbaba8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,8 +113,8 @@ def parseur_log_apache(log_apache, request): ParseurLogApache: Une instance de la classe :class:`ParseurLogApache`. """ if hasattr(request, "param") and request.param == False: - return ParseurLogApache(log_apache(False)) - return ParseurLogApache(log_apache(True)) + return ParseurLogApache(str(log_apache(False))) + return ParseurLogApache(str(log_apache(True))) @pytest.fixture() def fichier_log_apache(parseur_log_apache): @@ -140,8 +140,8 @@ def entree_log_apache(fichier_log_apache): ``lignes_valides``. Args: - fichier_log_apache (FichierLogApache): Fixture pour l'instance - de la classe :class:`FichierLogApache`. + fichier_log_apache (EntreeLogApache): Fixture pour l'instance + de la classe :class:`EntreeLogApache`. Returns: EntreeLogApache: Une instance de la classe :class:`EntreeLogApache`. diff --git a/tests/test_afficheur_cli.py b/tests/test_afficheur_cli.py index c68ec51..e5026fb 100644 --- a/tests/test_afficheur_cli.py +++ b/tests/test_afficheur_cli.py @@ -169,6 +169,23 @@ def test_afficheur_cli_lance_animation_chargement(mocker, afficheur_cli): assert afficheur_cli._thread_chargement is not None assert afficheur_cli._thread_chargement.is_alive() +def test_afficheur_cli_exception_stop_animation_chargment_type_invalide(afficheur_cli): + """ + Vérifie que la méthode stop_animation_chargement renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``erreur`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + afficheur_cli (AfficheurCLI): Fixture pour l'instance de la classe ``AfficheurCLI``. + """ + with pytest.raises(TypeError): + afficheur_cli.stop_animation_chargement("False") + def test_afficheur_cli_stop_animation_chargement_terminee(afficheur_cli): """ Vérifie que la méthode stop_animation_chargement arrête correctement l'animation en diff --git a/tests/test_exporteur.py b/tests/test_exporteur.py index f155bac..d5b6819 100644 --- a/tests/test_exporteur.py +++ b/tests/test_exporteur.py @@ -40,10 +40,27 @@ def test_exporteur_emplacement_inexistant(): with pytest.raises(ExportationException): exporteur = Exporteur("dossier/inexistant/sortie.json") +def test_exporteur_verification_exception_exportation_possible_type_invalide(exporteur): + """ + Vérifie que la méthode verification_exportation_possible renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``chemin_sortie`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + """ + with pytest.raises(TypeError): + exporteur.verification_exportation_possible(False) + @pytest.mark.parametrize("donnees", [ (0), (None), ([]) ]) -def test_exportateur_export_json_type_donnees_invalide(exporteur, donnees): +def test_exporteur_export_json_type_donnees_invalide(exporteur, donnees): """ Vérifie que la classe renvoie une erreur lorsque un argument de type invalide est passé dans la méthode ``export_vers_json``. @@ -66,7 +83,7 @@ def test_exportateur_export_json_type_donnees_invalide(exporteur, donnees): (FileNotFoundError("Fichier non trouvé.")), (Exception("Toutes exceptions")) ]) -def test_exportateur_export_json_exception_exportation(exporteur, mocker, exception): +def test_exporteur_export_json_exception_exportation(exporteur, mocker, exception): """ Vérifie que la classe renvoie l'exception :class:`ExportationException` lorsque une erreur apparait durant l'exportation des données. @@ -88,7 +105,7 @@ def test_exportateur_export_json_exception_exportation(exporteur, mocker, except with pytest.raises(ExportationException): exporteur.export_vers_json({}) -def test_exportateur_exportation_json_valide(exporteur, fichier_json): +def test_exporteur_exportation_json_valide(exporteur, fichier_json): """ Vérifie que la méthode ``export_vers_json`` exporte correctement les données vers une fichier. diff --git a/tests/test_fichier_entree_log_apache.py b/tests/test_fichier_entree_log_apache.py new file mode 100644 index 0000000..5831ac6 --- /dev/null +++ b/tests/test_fichier_entree_log_apache.py @@ -0,0 +1,86 @@ +""" +Module des tests unitaires pour la classe de représentation d'un fichier de log +ou celle d'une entrée d'un log. +""" + +import pytest +from datetime import datetime +from parse.fichier_log_apache import FichierLogApache +from parse.entree_log_apache import EntreeLogApache +from donnees.client_informations import ClientInformations +from donnees.requete_informations import RequeteInformations +from donnees.reponse_informations import ReponseInformations + + +@pytest.mark.parametrize("chemin, entrees", [ + (False, []), + ("Chemin", False) +]) +def test_fichier_log_exception_type_invalide(chemin, entrees): + """ + Vérifie que l'initialisation d'un FichierLogApache renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``chemin`` avec un mauvais type. + - Paramètre ``entrees`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + chemin (any): Le chemin du fichier. + entrees (any): Les entrées du fichier. + """ + with pytest.raises(TypeError): + fichier = FichierLogApache(chemin, entrees) + +def test_fichier_log_exception_ajoute_entree_type_invalide(fichier_log_apache): + """ + Vérifie que la méthode ajoute_entree retourne une erreur lorsque son paramètre + n'est pas du type attendu. + + Scénarios testés: + - Paramètre ``entree`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + fichier_log_apache (FichierLogApache): Fixture pour l'instance + de la classe :class:`FichierLogApache`. + """ + with pytest.raises(TypeError): + fichier_log_apache.ajoute_entree(False) + +@pytest.mark.parametrize("client, requete, reponse", [ + (False, + RequeteInformations(datetime(1, 1, 1), "GET", "/", "HTTP/1.1", "/"), + ReponseInformations(0, 0)), + (ClientInformations("1.1.1.1", "RFC", "test", "google"), + False, + ReponseInformations(0, 0)), + (ClientInformations("1.1.1.1", "RFC", "test", "google"), + RequeteInformations(datetime(1, 1, 1), "GET", "/", "HTTP/1.1", "/"), + False) +]) +def test_entree_log_exception_type_invalide(client, requete, reponse): + """ + Vérifie que l'initialisation d'un EntreeLogApache renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``client`` avec un mauvais type. + - Paramètre ``requete`` avec un mauvais type. + - Paramètre ``reponse`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + client (any): Les informations du client sur cette entrée. + requete (any): Les informations de la requête sur cette entrée. + reponse (any): Les informations de la réponse sur cette entrée. + """ + with pytest.raises(TypeError): + entree = EntreeLogApache(client, requete, reponse) \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index a92fba9..80d5f9c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,22 +3,25 @@ """ import pytest -import sys from main import main from cli.parseur_arguments_cli import ArgumentCLIException from parse.parseur_log_apache import FormatLogApacheInvalideException from export.exporteur import ExportationException -@pytest.mark.parametrize("exception", [ - (ArgumentCLIException), - (FileNotFoundError), - (FormatLogApacheInvalideException), - (ExportationException), - (Exception) -]) + +@pytest.mark.parametrize( + "exception", + [ + (ArgumentCLIException), + (FormatLogApacheInvalideException), + (ExportationException), + (TypeError), + (ValueError), + ], +) def test_main_gestion_exception(mocker, exception): """ - Vérifie que les exceptions sont interceptées dans fichier principal. + Vérifie que les exceptions attendues sont interceptées dans fichier principal. Scénarios testés: - Vérification que les exceptions n'arrête pas le programme. @@ -45,18 +48,22 @@ def test_main_succes(mocker): """ # Mock des classes pour simuler un fonctionnement correct mock_parseur_cli = mocker.patch("main.ParseurArgumentsCLI") - mock_parseur_cli.return_value.parse_args.return_value = mocker.MagicMock(chemin_log="test.log") - + mock_parseur_cli.return_value.parse_args.return_value = mocker.MagicMock( + chemin_log="test.log" + ) + mock_parseur_log = mocker.patch("main.ParseurLogApache") mock_parseur_log.return_value.parse_fichier.return_value = mocker.MagicMock() - + mock_analyseur_log = mocker.patch("main.AnalyseurLogApache") - mock_analyseur_log.return_value.get_analyse_complete.return_value = {"chemin": "test.log"} + mock_analyseur_log.return_value.get_analyse_complete.return_value = { + "chemin": "test.log" + } mocker.patch("main.Exporteur") - + # Vérifie qu'aucune exception n'est levée try: main() except Exception: - pytest.fail("Aucune exception ne doit être levée ici") \ No newline at end of file + pytest.fail("Aucune exception ne doit être levée ici") diff --git a/tests/test_parseur_arguments_cli.py b/tests/test_parseur_arguments_cli.py index 6d34ee6..0cf8fa9 100644 --- a/tests/test_parseur_arguments_cli.py +++ b/tests/test_parseur_arguments_cli.py @@ -36,6 +36,31 @@ # Tests unitaires +@pytest.mark.parametrize("args, namespace", [ + (False, None), + (None, False) +]) +def test_parseur_cli_exception_parse_args_type_invalide(parseur_arguments_cli, args, namespace): + """ + Vérifie que la méthode parse_args renvoie une erreur lorsque le type + d'un de ses paramètres est invalide. + + Scénarios testés: + - Paramètre ``args`` avec un mauvais type. + - Paramètre ``namespace`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurArgumentsCLI`. + args (Optional[list]): Une liste avec des arguments. + namespace (Optional[Namespace]): L'espace de nom où stocker les arguments. + """ + with pytest.raises(TypeError): + parseur_arguments_cli.parse_args(args, namespace) + @pytest.mark.parametrize("arguments", arguments_invalides) def test_parseur_cli_exception_argument_inconnu(parseur_arguments_cli, arguments): """ diff --git a/tests/test_parseur_log_apache.py b/tests/test_parseur_log_apache.py index fb6f1ce..eb0ca02 100644 --- a/tests/test_parseur_log_apache.py +++ b/tests/test_parseur_log_apache.py @@ -6,11 +6,27 @@ from re import match from datetime import datetime, timezone, timedelta from conftest import lignes_valides, lignes_invalides -from parse.parseur_log_apache import ParseurLogApache, FormatLogApacheInvalideException +from parse.parseur_log_apache import (ParseurLogApache, + FormatLogApacheInvalideException, + FichierLogApacheIntrouvableException) # Tests unitaires +def test_parseur_log_exception_type_invalide(): + """ + Vérifie que l'initialisation d'un ParseurLogApache renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``chemin_log`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + """ + with pytest.raises(TypeError): + ParseurLogApache(False) + def test_parseur_log_exception_fichier_introuvable(): """ Vérifie qu'une exception est bien levée lorsque le chemin du fichier @@ -22,7 +38,7 @@ def test_parseur_log_exception_fichier_introuvable(): Asserts: - Une exception :class:`FileNotFoundError` est levée. """ - with pytest.raises(FileNotFoundError): + with pytest.raises(FichierLogApacheIntrouvableException): parseur = ParseurLogApache("fichier/existe/pas.txt") @pytest.mark.parametrize("parseur_log_apache", [False], indirect=["parseur_log_apache"]) @@ -44,6 +60,24 @@ def test_parseur_log_exception_fichier_invalide(parseur_log_apache): with pytest.raises(FormatLogApacheInvalideException): fichier = parseur_log_apache.parse_fichier() +def test_parseur_log_exception_parse_entree_type_invalide(parseur_log_apache): + """ + Vérifie que la méthode parse_entree renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``entree`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurLogApache`. + """ + with pytest.raises(TypeError): + parseur_log_apache.parse_entree(False) + @pytest.mark.parametrize("ligne_invalide", lignes_invalides) def test_parseur_log_exception_entree_invalide(parseur_log_apache, ligne_invalide): """ @@ -81,6 +115,32 @@ def test_parseur_log_nombre_entrees_valide(parseur_log_apache): fichier_log = parseur_log_apache.parse_fichier() assert len(fichier_log.entrees) == len(lignes_valides) +@pytest.mark.parametrize("analyse_regex, nom_information", [ + (False, "Information"), + ({}, False) +]) +def test_parseur_log_apache_exception_get_information_entree_type_invalide( + parseur_log_apache, analyse_regex, nom_information): + """ + Vérifie que la méthode get_information_entree renvoie une erreur lorsque le type + d'un de ses paramètres est invalide. + + Scénarios testés: + - Paramètre ``analyse_regex`` avec un mauvais type. + - Paramètre ``nom_information`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurLogApache`. + analyse_regex (any): L'analyse de l'entrée par un regex. + nom_informations (any): Nom de l'information à récupérer. + """ + with pytest.raises(TypeError): + parseur_log_apache.get_information_entree(analyse_regex, nom_information) + @pytest.mark.parametrize("nom_information, retour_attendu", [ ("ip", "192.168.1.1"), ("rfc", None), @@ -143,3 +203,57 @@ def test_parsage_entree_valide(parseur_log_apache): assert entree.requete.ancienne_url == "/home" assert entree.reponse.code_statut_http == 200 assert entree.reponse.taille_octets == 532 + +def test_parseur_log_exception_extraction_informations_client_type_invalide(parseur_log_apache): + """ + Vérifie que la méthode _extraire_informations_client renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``analyse_regex`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurLogApache`. + """ + with pytest.raises(TypeError): + parseur_log_apache._extraire_informations_client(False) + +def test_parseur_log_exception_extraction_informations_requete_type_invalide(parseur_log_apache): + """ + Vérifie que la méthode _extraire_informations_requete renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``analyse_regex`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurLogApache`. + """ + with pytest.raises(TypeError): + parseur_log_apache._extraire_informations_requete(False) + +def test_parseur_log_exception_extraction_informations_reponse_type_invalide(parseur_log_apache): + """ + Vérifie que la méthode _extraire_informations_reponse renvoie une erreur lorsque le type + de son paramètre est invalide. + + Scénarios testés: + - Paramètre ``analyse_regex`` avec un mauvais type. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurLogApache`. + """ + with pytest.raises(TypeError): + parseur_log_apache._extraire_informations_reponse(False) \ No newline at end of file From 154089cadb5de76ce57c471f871e8c145681aaf0 Mon Sep 17 00:00:00 2001 From: AnthonyGuillauma Date: Sat, 5 Apr 2025 12:44:02 +0200 Subject: [PATCH 42/47] =?UTF-8?q?CI:=20Qualit=C3=A9=20du=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout d'un workflows pour tester que la note du code dans le dossier app est supérieure ou égale à 9 --- .github/workflows/qualite.yaml | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/qualite.yaml diff --git a/.github/workflows/qualite.yaml b/.github/workflows/qualite.yaml new file mode 100644 index 0000000..0dae2ee --- /dev/null +++ b/.github/workflows/qualite.yaml @@ -0,0 +1,42 @@ +name: Qualité code - LogBuster + +on: + push: + branches: + - develop + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout du code + uses: actions/checkout@v4 + + - name: Configuration de Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Installation des dépendances + run: | + python -m pip install --upgrade pip + pip install pylint + pip install colorama + + - name: Analyse avec Pylint (note >= 9.0 requise) + run: | + pylint app > tests-resultats-qualite.txt || true + SCORE=$(grep "Your code has been rated at" tests-resultats-qualite.txt | awk '{print $7}' | cut -d"/" -f1) + echo "Le score du code dans le dossier app est de $SCORE" + SCORE_VALIDE=$(echo "$SCORE >= 9.0" | bc) + if [ "$SCORE_VALIDE" -ne 1 ]; then + echo "Erreur: La note du code est inférieur à 9." + exit 1 + fi + + - name: Upload du rapport Pylint + uses: actions/upload-artifact@v4 + with: + name: rapport-qualite-code + path: tests-resultats-qualite.txt \ No newline at end of file From 2005900f7bb66c15f84fe1959742c5ed00e70a2e Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:14:37 +0200 Subject: [PATCH 43/47] =?UTF-8?q?Chore:=20Am=C3=A9lioration=20du=20README.?= =?UTF-8?q?md=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de la description du projet - Ajout d'une table des matières - Ajout de la partie des fonctionnalités - Ajout de la partie de l'utilisation de base - Ajout de la partie de la documentation - Ajout de la partie des tests unitaires - Ajout de la licence --- README.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0158b45..7a9c2aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,107 @@ # LogBuster +``` + .-. .-') .-') .-') _ ('-. _ .-') ,---. + \ ( OO ) ( OO ). ( OO) ) _( OO)( \( -O ) | | + ,--. .-'),-----. ,----. ;-----.\ ,--. ,--. (_)---\_)/ '._(,------.,------. | | + | |.-') ( OO' .-. ' ' .-./-') | .-. | | | | | / _ | |'--...__)| .---'| /`. '| | + | | OO )/ | | | | | |_( O- )| '-' /_) | | | .-') \ :` `. '--. .--'| | | / | || | + | |`-' |\_) | |\| | | | .--, \| .-. `. | |_|( OO ) '..`''.) | | (| '--. | |_.' || .' +(| '---.' \ | | | |(| | '. (_/| | \ | | | | `-' /.-._) \ | | | .--' | . '.'`--' + | | `' '-' ' | '--' | | '--' /(' '-'(_.-' \ / | | | `---.| |\ \ .--. + `------' `-----' `------' `------' `-----' `-----' `--' `------'`--' '--''--' +``` + Bienvenue dans le monde de LogBuster, l'outil ultime pour analyser, décortiquer et sauver vos logs Apache des griffes du chaos. Vous avez des logs qui traînent, qui sont indéchiffrables ou tout simplement encombrants ? Pas de panique, LogBuster est là pour les attraper, les analyser et vous offrir des statistiques claires et précises, comme jamais auparavant ! -Inspiré par l’univers de Ghostbusters, LogBuster chasse les erreurs et anomalies des logs Apache avec la même efficacité que les chasseurs de fantômes. Que ce soit pour des fichiers volumineux ou des logs complexes, LogBuster est votre équipe d’intervention spécialisée. Plus de confusion, plus de stress – vos logs sont en sécurité ! \ No newline at end of file +## 📋 Table des matières + +- [👻 Fonctionnalités](#-fonctionnalités) +- [📦 Installation](#-installation) +- [🛠️ Utilisation de base](#️-utilisation-de-base) +- [📖 Documentation](#-documentation) +- [🧪 Lancer les tests](#-lancer-les-tests) +- [📜 Licence](#-licence) + +## 👻 Fonctionnalités + +- 📄 Parsing avancé de logs Apache. +- 📉 Extraire des statistiques clés. +- 🗂️ Ranger les données par catégorie. +- 🧹 Indiquer les erreurs de format avec précision. +- 🚚 Exporter les données en JSON. + +## 📦 Installation + +### Bash (linux/macOS) +```bash +git clone https://github.com/AnthonyGuillauma/code_source +cd code_source +python -m venv .venv +source .venv/bin/activate # Activation de l'environnement virtuel sous Bash +pip install -r requirements.txt +``` + +### Windows (cmd) +```bash +git clone https://github.com/AnthonyGuillauma/code_source +cd code_source +python -m venv .venv +.venv\Scripts\activate # Activation de l'environnement virtuel sous Windows +pip install -r requirements.txt +``` + +## 🛠️ Utilisation de base + +``` +python app/main.py chemin_log [-s SORTIE] +``` +- `chemin_log` : Le chemin vers le fichier de log Apache à analyser. +- `-s SORTIE` (optionnel) : Le chemin où sauvegarder les résultats de l'analyse. Si non spécifié, les résultats seront sauvegardés dans un fichier `analyse-log-apache.json`. + +## 📖 Documentation + +La documentation complète du code du projet se situe [ici](https://anthonyguillauma.github.io/code_source/ +). + +Si vous souhaitez la générer vous même, suivez ces étapes : + +Tout d'abord, placez-vous dans le dossier `docs` qui contient les fichiers sources de la documentation : + +```bash +cd docs +``` + +Puis, générez la documentation via la commande suivante : + +```bash +./make html +``` + +Enfin, ouvrez le fichier html `build/html/index.html` généré dans un navigateur. + +## 🧪 Lancer les tests + +Les tests unitaires du projet peuvent être exécutés avec pytest. Pour lancer les tests, assurez-vous d'avoir activé l'environnement virtuel et installé les dépendances. + +Premièrement, placez-vous dans le dossier `tests` qui contient les fichiers de configuration pour les tests unitaires : + +```bash +cd tests +``` + +Ensuite, exécutez les tests avec la commande suivante : + +```bash +pytest +``` + +Enfin, si vous souhaitez également afficher la couverture des tests unitaires, utilisez la commande suivante : + +```bash +pytest --cov=../app --cov-report=term-missing +``` + +# 📜 Licence + +Ce projet est sous licence MIT. \ No newline at end of file From bf428a31b45ba8d2a9efecc8021c5094793fb878 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:41:19 +0200 Subject: [PATCH 44/47] Chore: Ajout de requirements.txt (#36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout du fichier requirements.txt qui contient les dépendances du projet - Ajout de la dépendance colorama dans requirements.txt - Ajout de la dépendance sphinx et de ses dépendances dans requirements.txt - Ajout de la dépendance pytest et de ses dépendances dans requirements.txt --- requirements.txt | Bin 0 -> 724 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2c8dc61cd3fe3215c38d157d4c3ea38d33c7fd66 GIT binary patch literal 724 zcmbVK+X{kE5S-_rPcgLnOJC6!m_}AwUMi}OuVz-MLq-8WSNW{{t$cXN0Y( zNhz>AHFAy|3D(%*{B-LRGccXX>3Yn3Kn#!Xg!!^2y%go#Z!vV%9nmHA7vzOh%Q>H= z@l*aQx!hjKUB`EROy=oNZo(WHy_MW`4$NJf`R2Sqfg_nEccaYOXT7cOtjcbI<|S6` r^$W9TX6z>YZAO))Tyxebr+&4~uIOK%zCCZ@LT)Ft`=%4RNqsy3;7WGa literal 0 HcmV?d00001 From c773d5071455f6723b9f2e984373072e9100b1a6 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 5 Apr 2025 16:00:29 +0200 Subject: [PATCH 45/47] =?UTF-8?q?Chore:=20Ajout=20de=20la=20pr=C3=A9cautio?= =?UTF-8?q?n=20d'affichage=20dans=20le=20README.md=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout d'une précaution concernant l'affichage de caractères unicodes par l'application dans le terminal --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 7a9c2aa..e02d302 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Bienvenue dans le monde de LogBuster, l'outil ultime pour analyser, décortiquer - [👻 Fonctionnalités](#-fonctionnalités) - [📦 Installation](#-installation) - [🛠️ Utilisation de base](#️-utilisation-de-base) +- [⚠️ Précautions](#️-précautions) - [📖 Documentation](#-documentation) - [🧪 Lancer les tests](#-lancer-les-tests) - [📜 Licence](#-licence) @@ -59,6 +60,15 @@ python app/main.py chemin_log [-s SORTIE] - `chemin_log` : Le chemin vers le fichier de log Apache à analyser. - `-s SORTIE` (optionnel) : Le chemin où sauvegarder les résultats de l'analyse. Si non spécifié, les résultats seront sauvegardés dans un fichier `analyse-log-apache.json`. +## ⚠️ Précautions + +Le projet LogBuster utilise des caractères Unicode, tels que des symboles spéciaux, dans le terminal pour rendre l'affichage plus plaisant. Assurez-vous que votre terminal est configuré pour prendre en charge l'affichage de caractères Unicode afin de profiter pleinement de l'expérience utilisateur. + +Si vous rencontrez des problèmes d'affichage (comme des symboles manquants ou mal rendus), vous pouvez essayer les solutions suivantes : + +- Utiliser un terminal compatible avec Unicode (par exemple, Terminal sous macOS, Windows Terminal sous Windows, ou des terminaux comme GNOME Terminal ou Konsole sous Linux). +- Vérifier que votre terminal utilise une police qui prend en charge les caractères Unicode (par exemple, DejaVu Sans Mono ou Consolas). + ## 📖 Documentation La documentation complète du code du projet se situe [ici](https://anthonyguillauma.github.io/code_source/ From 82677b8088a4291976c1996c41a3cc9598914f76 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 5 Apr 2025 17:00:20 +0200 Subject: [PATCH 46/47] =?UTF-8?q?Docs:=20Am=C3=A9lioration=20de=20la=20pag?= =?UTF-8?q?e=20d'accueil=20de=20la=20documentation=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de la partie utilisation - Ajout de la partie du format de log Apache - Ajout de la partie prévention de l'affichage d'unicode --- docs/source/index.rst | 94 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index d21ae8e..c15e472 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,14 +1,94 @@ .. Documentation de LogBuster -Bienvenue dans la documentation de LogBuster -============================================ +👻 Bienvenue dans la documentation de **LogBuster** +==================================================== -LogBuster est un outil puissant pour analyser les fichiers de log Apache. -Il vous permet de : +**LogBuster** est un outil puissant pour analyser les fichiers de logs Apache. +Grâce à LogBuster, vous pouvez extraire des informations essentielles, gérer des erreurs de format et bien plus encore. -- Extraire des statistiques clés. -- Exporter les données en JSON. -- Gérer les erreurs de format avec précision. +.. code:: text + + .-. .-') .-') .-') _ ('-. _ .-') ,---. + \ ( OO ) ( OO ). ( OO) ) _( OO)( \( -O ) | | + ,--. .-'),-----. ,----. ;-----.\ ,--. ,--. (_)---\_)/ '._(,------.,------. | | + | |.-') ( OO' .-. ' ' .-./-') | .-. | | | | | / _ | |'--...__)| .---'| /`. '| | + | | OO )/ | | | | | |_( O- )| '-' /_) | | | .-') \ :` `. '--. .--'| | | / | || | + | |`-' |\_) | |\| | | | .--, \| .-. `. | |_|( OO ) '..`''.) | | (| '--. | |_.' || .' + (| '---.' \ | | | |(| | '. (_/| | \ | | | | `-' /.-._) \ | | | .--' | . '.'`--' + | | `' '-' ' | '--' | | '--' /(' '-'(_.-' \ / | | | `---.| |\ \ .--. + `------' `-----' `------' `------' `-----' `-----' `--' `------'`--' '--''--' + +**(҂-_•)⊃═O Fonctionnalités principales** +------------------------------------------- + +- **Extraire des statistiques clés** : Obtenez des données précieuses sur vos fichiers de logs. +- **Exporter les données en JSON** : Accédez à un format structuré pour vos analyses. +- **Gérer les erreurs de format avec précision** : Identifiez rapidement les anomalies et les erreurs de vos fichiers log. + +**d(■᎑■⌐) Utilisation** +--------------------------- + +``` +python app/main.py chemin_log [-s SORTIE] +``` + +- `chemin_log` : Le chemin vers le fichier de log Apache à analyser. +- `-s SORTIE` (optionnel) : Le chemin où sauvegarder les résultats de l'analyse. Si non spécifié, les résultats seront sauvegardés dans un fichier `analyse-log-apache.json`. + +**(ง'̀᎑'́)ง Format des fichier de log Apache** +------------------------------------------------ + +Le format de log Apache pris en charge est celui utilisé par le fichier `access.log`. +Ces logs Apache contiennent des informations détaillées sur les requêtes HTTP traitées par le serveur. +Chaque ligne d'un fichier représente une requête individuelle, et les informations sont généralement séparées par des espaces ou des caractères spécifiques. + +Format commun (Common Log Format) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Un fichier de log Apache standard suit généralement un format similaire au suivant : + +``127.0.0.1 - - [10/Oct/2025:13:55:36 +0000] "GET /index.html HTTP/1.1" 200 2326`` + +1. **IP de l'utilisateur** (127.0.0.1) : L'adresse IP de l'utilisateur qui a effectué la requête. +2. **Identifiant de l'utilisateur** (vide ici) : L'identifiant de l'utilisateur, souvent vide ou un tiret (`-`). +3. **Identifiant de l'utilisateur authentifié** (vide ici) : Si l'utilisateur est authentifié, cet identifiant sera visible, sinon il sera également vide ou un tiret (`-`). +4. **Date et heure de la requête** ([10/Oct/2025:13:55:36 +0000]) : La date et l'heure précises de la requête, suivies du fuseau horaire. +5. **Requête HTTP** ("GET /index.html HTTP/1.1") : La méthode HTTP utilisée (ici `GET`), l'URL demandée (ici `/index.html`), et la version du protocole HTTP (ici `HTTP/1.1`). +6. **Code de statut HTTP** (200) : Le code de statut retourné par le serveur (ici, `200` indique que la requête a réussi). +7. **Taille de la réponse** (2326) : La taille en octets de la réponse envoyée au client. + +Dans LogBuster, l'**IP de l'utilisateur** et la **Date et heure de la requête** sont obligatoires, sinon un message d'erreur est retournée. + +Format combiné (Combined Log Format) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Le format étendu permet d'ajouter plus de détails sur chaque requête. +Voici un exemple de ligne de log avec ce format : + +``127.0.0.1 - - [10/Oct/2025:13:55:36 +0000] "GET /index.html HTTP/1.1" 200 2326 "http://referrer.com" "Mozilla/5.0"`` + +Dans ce format, deux informations supplémentaires à la fin de l'entrée sont présentes : + +1. **Référent HTTP** ("http://referrer.com") : L'URL de la page depuis laquelle la requête a été faite. Cela peut être vide si la requête provient directement de l'utilisateur sans référence. +2. **Agent utilisateur** ("Mozilla/5.0") : L'agent utilisateur indique quel navigateur ou appareil a effectué la requête. + +Assurez-vous que votre fichier de log Apache suit un format cohérent, comme ceux mentionnés ci-dessus, afin d'obtenir des résultats précis et fiables lors de l'utilisation de LogBuster. +Pour plus d'informations, consultez la documentation Apache sur ce lien : https://httpd.apache.org/docs/2.4/fr/logs.html + +**(∩x_x) Précautions concernant l'affichage des caractères spéciaux** +---------------------------------------------------------------------- + +Le projet LogBuster utilise des caractères **Unicode**, tels que des symboles spéciaux, dans le terminal pour rendre l'affichage plus plaisant. Assurez-vous que votre terminal est configuré pour prendre en charge l'affichage de caractères Unicode afin de profiter pleinement de l'expérience utilisateur. + +Si vous rencontrez des problèmes d'affichage (comme des symboles manquants ou mal rendus), vous pouvez essayer les solutions suivantes : + +- Utiliser un terminal compatible avec Unicode (par exemple, Terminal sous macOS, Windows Terminal sous Windows, ou des terminaux comme GNOME Terminal ou Konsole sous Linux). +- Vérifier que votre terminal utilise une police qui prend en charge les caractères Unicode (par exemple, DejaVu Sans Mono ou Consolas). + +**ε=(( ꐑº-° )ꐑ Documentation du projet** +------------------------------------------- + +Consultez les différentes sections pour en savoir plus sur le projet **LogBuster** : .. toctree:: :maxdepth: 4 From c9cf0ac7016198713a87f1598ad19c52b5fdfcd9 Mon Sep 17 00:00:00 2001 From: Anthony GUILLAUMA <146327509+AnthonyGuillauma@users.noreply.github.com> Date: Sat, 5 Apr 2025 17:25:13 +0200 Subject: [PATCH 47/47] CI: Mise en place des workflows sur des pull requests (#39) - Mise en place du workflow de la documentation sur une pull request vers la branche main - Mise en place du workflow des tests unitaires sur une pull request vers la branche main ou develop - Mise en place du workflow de la qualite du code sur une pull request vers la branche main ou develop - Ajout de commentaires sur les workflows --- .github/workflows/documentation.yaml | 15 +++++++++++---- .github/workflows/qualite.yaml | 8 +++++++- .github/workflows/tests.yaml | 3 ++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 7979dd1..d47ea1c 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -3,26 +3,30 @@ name: Documentation - LogBuster on: push: branches: - - develop + - main +# Permissions (lecture et écriture sur la GitHub Page) permissions: contents: read pages: write - id-token: write # Nécessaire pour déployer sur GitHub Pages + id-token: write jobs: build: runs-on: ubuntu-latest steps: + # Etape 1: Cloner le dépôt - name: Positionnement sur le dépôt uses: actions/checkout@v4 + # Étape 2 : Installer Python - name: Mis en place de Python uses: actions/setup-python@v4 with: python-version: '3.x' + # Étape 3 : Installer les dépendances - name: Installation des dépendances run: | python -m pip install --upgrade pip @@ -30,14 +34,16 @@ jobs: pip install sphinx_rtd_theme --break-system-packages pip install colorama + # Étape 4 : Générer la documentation - name: Construction de la documentation (avec Sphinx) run: | sphinx-build -b html docs/source docs/build/html - - name: Publication la documentation générée + # Étape 5 : Sauvegarder l'artefact (pour la GitHub Page) + - name: Sauvegarder la documentation uses: actions/upload-pages-artifact@v3 with: - path: docs/build/html # Dossier contenant la doc générée + path: docs/build/html deploy: needs: build @@ -47,6 +53,7 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: + # Déploiement de la documentation - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/qualite.yaml b/.github/workflows/qualite.yaml index 0dae2ee..6e6ad7b 100644 --- a/.github/workflows/qualite.yaml +++ b/.github/workflows/qualite.yaml @@ -1,8 +1,9 @@ name: Qualité code - LogBuster on: - push: + pull_request: branches: + - main - develop jobs: @@ -10,20 +11,24 @@ jobs: runs-on: ubuntu-latest steps: + # Étape 1 : Cloner le dépôt - name: Checkout du code uses: actions/checkout@v4 + # Étape 2 : Installer Python - name: Configuration de Python uses: actions/setup-python@v4 with: python-version: '3.x' + # Étape 3 : Installer les dépendances - name: Installation des dépendances run: | python -m pip install --upgrade pip pip install pylint pip install colorama + # Étape 4 : Lancement de l'analyse - name: Analyse avec Pylint (note >= 9.0 requise) run: | pylint app > tests-resultats-qualite.txt || true @@ -35,6 +40,7 @@ jobs: exit 1 fi + # Sauvegarder l'artefact - name: Upload du rapport Pylint uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e400e16..13b33c9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,8 +1,9 @@ name: Tests unitaires - LogBuster on: - push: + pull_request: branches: + - main - develop # Permissions (lecture uniquement)