diff --git a/.gitignore b/.gitignore index 7a65ece..b103cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .venv/ -output/ +output/* src/logwatcher/__pycache__/ *.log -__pycache__ \ No newline at end of file +__pycache__ +docs/convention* +docs/resultats.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 46f4007..bfaa7b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,7 @@ { "version": "0.2.0", "configurations": [ + { "name": "Python Debugger: Current File", "type": "debugpy", @@ -15,7 +16,31 @@ "cwd": "${workspaceFolder}", "console": "integratedTerminal", "module": "logwatcher.app", - //"program": "${workspaceFolder}\\.venv\\Scripts\\logwatcher.exe" + }, + { + "name": "Python Test: Current File", + "type": "debugpy", + "request": "launch", + "program": "pytest", + "args": [ + "-p", + "vscode_pytest", + "--rootdir=${workspaceFolder}", + "--capture=no", + "${file}" + ] + }, + { + "name": "Python Test: All tests", + "type": "debugpy", + "request": "launch", + "program": "pytest", + "args": [ + "-p", + "vscode_pytest", + "--rootdir=${workspaceFolder}", + "--capture=no" + ] } ] } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4577fac..0855b6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,11 @@ version = "0.1.0" description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif." readme = "README.md" requires-python = ">=3.10" -dependencies = [ +dependencies = [] + +[dependency-groups] +dev = [ + "ruff>=0.16.0", "mypy>=2.3.0", "pytest>=9.1.1", ] @@ -12,11 +16,6 @@ dependencies = [ [project.scripts] logwatcher = "logwatcher.app:app" -[project.optional-dependencies] -dev = [ - "ruff>=0.16.0", - "mypy>=2.3.0", -] [build-system] requires = ["hatchling"] diff --git a/src/logwatcher/__init__.py b/src/logwatcher/__init__.py index 1898185..2ec9f74 100644 --- a/src/logwatcher/__init__.py +++ b/src/logwatcher/__init__.py @@ -1,3 +1,4 @@ +from logwatcher.config import * from logwatcher.logging import setup_logging logger = setup_logging() diff --git a/src/logwatcher/config.py b/src/logwatcher/config.py index 694ce31..d3f1907 100644 --- a/src/logwatcher/config.py +++ b/src/logwatcher/config.py @@ -1,9 +1,40 @@ +import re +from datetime import timedelta, timezone from pathlib import Path +### GENERAL DATA # output path OUTPUT_PATH = Path("output") RESULT_PATH = OUTPUT_PATH / "results" LOGGING_PATH = OUTPUT_PATH / "logs" -# fixture path -FIXTURE_PATH = Path.cwd() / "fixtures" \ No newline at end of file +# test path +TEST_PATH = Path("tests") +FIXTURE_PATH = TEST_PATH / "fixtures" + +# date and time +DATETIME_FORMAT = "%d/%m/%Y %H:%M:%S" +FRENCH_TIMEZONE = timezone(offset=timedelta(hours=2)) # UTC+2 = CEST + + +### LOG DATA +### definition of log pattern +SERVER_IP_PATTERN = r"([1-9]\d{1,2}\.)(0{1}|\d{1,3}\.){2}(0{1}|\d{1,3})" +MDC_SERVER_NAME_PATTERN = r"MDC_\d+" +DATE_TIME_PATTERN = r"((0[1-9])|([1-2][0-9])|(3[0-1]))/((0[1-9])|(1[0-2]))/\d{4} (([0-1][0-9])|(2[0-3])):[0-5][0-9]:[0-5][0-9]" +STORE_NAME_PATTERN = r"[^\[]+" +ERROR_MESSAGE_PATTERN = r".*" + +LOG_PATTERN = re.compile( + r"\\\\" + rf"(?P{SERVER_IP_PATTERN})" + r"\\[^\\]+\\" + rf"(?P{MDC_SERVER_NAME_PATTERN})" + r"[\\\w ]+\.\w+ \[" + rf"(?P{DATE_TIME_PATTERN})" + r"\] DOSSIER EN COURS : " + rf"(?P{STORE_NAME_PATTERN})" + rf" \[(?P{DATE_TIME_PATTERN})\] " + rf"(?P{ERROR_MESSAGE_PATTERN})", + re.VERBOSE +) \ No newline at end of file diff --git a/src/logwatcher/models.py b/src/logwatcher/models.py new file mode 100644 index 0000000..ada80bb --- /dev/null +++ b/src/logwatcher/models.py @@ -0,0 +1,62 @@ +import re +from dataclasses import dataclass +from datetime import datetime + +from logwatcher import DATETIME_FORMAT, FRENCH_TIMEZONE +from logwatcher.config import ( + DATE_TIME_PATTERN, + ERROR_MESSAGE_PATTERN, + MDC_SERVER_NAME_PATTERN, + SERVER_IP_PATTERN, + STORE_NAME_PATTERN, +) + + +@dataclass() +class LogEntry: + """ + Represents a line in log file. + """ + server_ip: str + mdc_server_name: str + start_time: datetime + store_name: str + error_time: datetime + error_message: str + raw_line: str # full initial log + + def __str__(self): + return self.error_message + + def get_full_message(self): + return self.raw_line + + def get_start_time(self): + return datetime.strftime(DATETIME_FORMAT, tz=FRENCH_TIMEZONE) + + def __post_init__(self): + """ + Each property must be validated by specific regex defined in config.py + """ + if not re.search(SERVER_IP_PATTERN, self.server_ip): + raise ValueError(f"Server IP '{self.server_ip}' must be a valid IPV4.") + + if not re.search(MDC_SERVER_NAME_PATTERN, self.mdc_server_name): + raise ValueError(f"MDC server name '{self.mdc_server_name}' must be in format {MDC_SERVER_NAME_PATTERN}.") + + if not re.search(DATE_TIME_PATTERN, self.start_time.strftime(DATETIME_FORMAT)): + raise ValueError(f"Start time '{self.start_time}' must be in format {DATE_TIME_PATTERN}.") + + if not re.search(STORE_NAME_PATTERN, self.store_name): + raise ValueError(f"Store name '{self.store_name}' must be a string with only alphanumeric characters, '-', '_', or ' '.") + + if not re.search(DATE_TIME_PATTERN, self.error_time.strftime(DATETIME_FORMAT)): + raise ValueError(f"Error time '{self.error_time}' must be in format {DATE_TIME_PATTERN}.") + + if not re.search(ERROR_MESSAGE_PATTERN, self.error_message): + raise ValueError("No error message found.") + + ### LOGIC ERRORS ### + # error time cannot be earlier than start time + if self.start_time > self.error_time: + raise ValueError(f"Error in date-times : error_time '{self.error_time}' cannot be earlier than start_time '{self.start_time}") diff --git a/fixtures/CR.txt b/tests/fixtures/CR.txt similarity index 100% rename from fixtures/CR.txt rename to tests/fixtures/CR.txt diff --git a/fixtures/CR_20260718150816.txt b/tests/fixtures/CR_20260718150816.txt similarity index 100% rename from fixtures/CR_20260718150816.txt rename to tests/fixtures/CR_20260718150816.txt diff --git a/fixtures/CR_20260727110008.txt b/tests/fixtures/CR_20260727110008.txt similarity index 100% rename from fixtures/CR_20260727110008.txt rename to tests/fixtures/CR_20260727110008.txt diff --git a/fixtures/CR_20260727110725.txt b/tests/fixtures/CR_20260727110725.txt similarity index 100% rename from fixtures/CR_20260727110725.txt rename to tests/fixtures/CR_20260727110725.txt diff --git a/fixtures/CR_20260727190008.txt b/tests/fixtures/CR_20260727190008.txt similarity index 100% rename from fixtures/CR_20260727190008.txt rename to tests/fixtures/CR_20260727190008.txt diff --git a/fixtures/CR_20260727190805.txt b/tests/fixtures/CR_20260727190805.txt similarity index 100% rename from fixtures/CR_20260727190805.txt rename to tests/fixtures/CR_20260727190805.txt diff --git a/fixtures/CR_20260728110838.txt b/tests/fixtures/CR_20260728110838.txt similarity index 100% rename from fixtures/CR_20260728110838.txt rename to tests/fixtures/CR_20260728110838.txt diff --git a/fixtures/CR_bl_auto_bestseller.txt b/tests/fixtures/CR_bl_auto_bestseller.txt similarity index 100% rename from fixtures/CR_bl_auto_bestseller.txt rename to tests/fixtures/CR_bl_auto_bestseller.txt diff --git a/fixtures/CR_rejet_taille.txt b/tests/fixtures/CR_rejet_taille.txt similarity index 100% rename from fixtures/CR_rejet_taille.txt rename to tests/fixtures/CR_rejet_taille.txt diff --git a/fixtures/CR_tva.txt b/tests/fixtures/CR_tva.txt similarity index 100% rename from fixtures/CR_tva.txt rename to tests/fixtures/CR_tva.txt diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..a120aa4 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,53 @@ +from datetime import datetime + +import pytest + +from logwatcher import DATETIME_FORMAT, FRENCH_TIMEZONE +from logwatcher.models import LogEntry + + +def test_valid_log_entry(): + LogEntry( + server_ip="192.168.13.27", + mdc_server_name="MDC_720", + start_time=datetime.strptime("18/07/2026 11:34:41", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + store_name="PECCOUD", + error_time=datetime.strptime("18/07/2026 11:35:58", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + error_message="Erreur FTP SDUpdatePrixISF : Requested action not taken", + raw_line="\\\\192.168.13.27\\e\\MDC_720\\Logs\\26\\07\\18\\20260718113428.txt [18/07/2026 11:34:41] DOSSIER EN COURS : PECCOUD [18/07/2026 11:35:58] Erreur FTP SDUpdatePrixISF : Requested action not taken" + ) + +def test_log_entry_invalid_dates(): + with pytest.raises(ValueError, match="Error in date-times"): + LogEntry( + server_ip="192.168.13.27", + mdc_server_name="MDC_720", + start_time=datetime.strptime("18/07/2026 11:35:58", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + store_name="PECCOUD", + error_time=datetime.strptime("18/07/2026 11:34:41", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + error_message="Erreur FTP SDUpdatePrixISF : Requested action not taken", + raw_line="\\\\192.168.13.27\\e\\MDC_720\\Logs\\26\\07\\18\\20260718113428.txt [18/07/2026 11:34:41] DOSSIER EN COURS : PECCOUD [18/07/2026 11:35:58] Erreur FTP SDUpdatePrixISF : Requested action not taken" + ) + +def test_equals_models(): + log_entry_1 = LogEntry( + server_ip="192.168.13.27", + mdc_server_name="MDC_720", + start_time=datetime.strptime("18/07/2026 11:34:41", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + store_name="PECCOUD", + error_time=datetime.strptime("18/07/2026 11:35:58", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + error_message="Erreur FTP SDUpdatePrixISF : Requested action not taken", + raw_line="\\\\192.168.13.27\\e\\MDC_720\\Logs\\26\\07\\18\\20260718113428.txt [18/07/2026 11:34:41] DOSSIER EN COURS : PECCOUD [18/07/2026 11:35:58] Erreur FTP SDUpdatePrixISF : Requested action not taken" + ) + + log_entry_2 = LogEntry( + server_ip="192.168.13.27", + mdc_server_name="MDC_720", + start_time=datetime.strptime("18/07/2026 11:34:41", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + store_name="PECCOUD", + error_time=datetime.strptime("18/07/2026 11:35:58", DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE), + error_message="Erreur FTP SDUpdatePrixISF : Requested action not taken", + raw_line="\\\\192.168.13.27\\e\\MDC_720\\Logs\\26\\07\\18\\20260718113428.txt [18/07/2026 11:34:41] DOSSIER EN COURS : PECCOUD [18/07/2026 11:35:58] Erreur FTP SDUpdatePrixISF : Requested action not taken" + ) + + assert log_entry_1 == log_entry_2 diff --git a/uv.lock b/uv.lock index 37860b8..2713a5e 100644 --- a/uv.lock +++ b/uv.lock @@ -168,25 +168,22 @@ wheels = [ name = "logwatcher" version = "0.1.0" source = { editable = "." } -dependencies = [ - { name = "mypy" }, - { name = "pytest" }, -] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "pytest" }, { name = "ruff" }, ] [package.metadata] -requires-dist = [ + +[package.metadata.requires-dev] +dev = [ { name = "mypy", specifier = ">=2.3.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.3.0" }, { name = "pytest", specifier = ">=9.1.1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.16.0" }, + { name = "ruff", specifier = ">=0.16.0" }, ] -provides-extras = ["dev"] [[package]] name = "mypy"