feat(log_entry): add model and tests
This commit is contained in:
+3
-1
@@ -1,6 +1,8 @@
|
||||
.venv/
|
||||
output/
|
||||
output/*
|
||||
src/logwatcher/__pycache__/
|
||||
*.log
|
||||
|
||||
__pycache__
|
||||
docs/convention*
|
||||
docs/resultats.json
|
||||
Vendored
+26
-1
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-6
@@ -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"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from logwatcher.config import *
|
||||
from logwatcher.logging import setup_logging
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
@@ -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"
|
||||
# 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>{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
|
||||
)
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user