feat(log_entry): add model and tests

This commit is contained in:
2026-08-03 13:30:03 +02:00
parent 63893513ee
commit 10602be7a3
18 changed files with 190 additions and 20 deletions
+3 -1
View File
@@ -1,6 +1,8 @@
.venv/ .venv/
output/ output/*
src/logwatcher/__pycache__/ src/logwatcher/__pycache__/
*.log *.log
__pycache__ __pycache__
docs/convention*
docs/resultats.json
+26 -1
View File
@@ -1,6 +1,7 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python Debugger: Current File", "name": "Python Debugger: Current File",
"type": "debugpy", "type": "debugpy",
@@ -15,7 +16,31 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"console": "integratedTerminal", "console": "integratedTerminal",
"module": "logwatcher.app", "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"
]
} }
] ]
} }
+5 -6
View File
@@ -4,7 +4,11 @@ version = "0.1.0"
description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif." description = "Trier, classifier et extraire automatiquement les entrées pertinentes d'un fichier de logs applicatif."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = []
[dependency-groups]
dev = [
"ruff>=0.16.0",
"mypy>=2.3.0", "mypy>=2.3.0",
"pytest>=9.1.1", "pytest>=9.1.1",
] ]
@@ -12,11 +16,6 @@ dependencies = [
[project.scripts] [project.scripts]
logwatcher = "logwatcher.app:app" logwatcher = "logwatcher.app:app"
[project.optional-dependencies]
dev = [
"ruff>=0.16.0",
"mypy>=2.3.0",
]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
+1
View File
@@ -1,3 +1,4 @@
from logwatcher.config import *
from logwatcher.logging import setup_logging from logwatcher.logging import setup_logging
logger = setup_logging() logger = setup_logging()
+33 -2
View File
@@ -1,9 +1,40 @@
import re
from datetime import timedelta, timezone
from pathlib import Path from pathlib import Path
### GENERAL DATA
# output path # output path
OUTPUT_PATH = Path("output") OUTPUT_PATH = Path("output")
RESULT_PATH = OUTPUT_PATH / "results" RESULT_PATH = OUTPUT_PATH / "results"
LOGGING_PATH = OUTPUT_PATH / "logs" LOGGING_PATH = OUTPUT_PATH / "logs"
# fixture path # test path
FIXTURE_PATH = Path.cwd() / "fixtures" 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>{SERVER_IP_PATTERN})"
r"\\[^\\]+\\"
rf"(?P<mdc_server_name>{MDC_SERVER_NAME_PATTERN})"
r"[\\\w ]+\.\w+ \["
rf"(?P<start_time>{DATE_TIME_PATTERN})"
r"\] DOSSIER EN COURS : "
rf"(?P<store_name>{STORE_NAME_PATTERN})"
rf" \[(?P<error_time>{DATE_TIME_PATTERN})\] "
rf"(?P<error_message>{ERROR_MESSAGE_PATTERN})",
re.VERBOSE
)
+62
View File
@@ -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}")
View File
View File
+53
View File
@@ -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
Generated
+6 -9
View File
@@ -168,25 +168,22 @@ wheels = [
name = "logwatcher" name = "logwatcher"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [
{ name = "mypy" },
{ name = "pytest" },
]
[package.optional-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "mypy" }, { name = "mypy" },
{ name = "pytest" },
{ name = "ruff" }, { name = "ruff" },
] ]
[package.metadata] [package.metadata]
requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=2.3.0" }, { name = "mypy", specifier = ">=2.3.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=2.3.0" },
{ name = "pytest", specifier = ">=9.1.1" }, { 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]] [[package]]
name = "mypy" name = "mypy"