refactor: Extract fail2ban response utilities into shared module

Consolidate duplicate _ok(), _to_dict(), ensure_list(), and is_not_found_error()
functions from 6 service modules into a single canonical implementation at
backend/app/utils/fail2ban_response.py.

Changes:
- Create fail2ban_response.py with canonical implementations
- Remove local duplicates from: ban_service, jail_service, config_service,
  health_service, server_service, config_file_utils
- Update all imports to use shared module
- Add comprehensive docstrings and examples
- Update Architecture.md and Backend-Development.md documentation

Benefits:
- Single source of truth for response parsing logic
- Eliminates code duplication across service layer
- Improves maintainability and consistency
- Enables centralized bug fixes and improvements

Tests: All 228 service tests passing, no regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-23 15:11:21 +02:00
parent 6d21a53620
commit b634ce876a
11 changed files with 832 additions and 444 deletions

View File

@@ -8,7 +8,6 @@ from __future__ import annotations
import asyncio
import re
from pathlib import Path
from typing import cast
import structlog
@@ -26,8 +25,8 @@ from app.utils.fail2ban_client import (
Fail2BanClient,
Fail2BanConnectionError,
Fail2BanProtocolError,
Fail2BanResponse,
)
from app.utils.fail2ban_response import ok
log: structlog.stdlib.BoundLogger = structlog.get_logger()
@@ -40,21 +39,6 @@ _NON_FILE_LOG_TARGETS: frozenset[str] = frozenset(
_SAFE_LOG_PREFIXES: tuple[str, ...] = ("/var/log", "/config/log")
def _ok(response: object) -> object:
"""Extract the payload from a fail2ban ``(return_code, data)`` response."""
try:
code, data = cast("Fail2BanResponse", response)
except (TypeError, ValueError) as exc:
raise ValueError(
f"Unexpected fail2ban response shape: {response!r}"
) from exc
if code != 0:
raise ValueError(f"fail2ban returned error code {code}: {data!r}")
return data
def _count_file_lines(file_path: str) -> int:
"""Count the total number of lines in *file_path* synchronously."""
count = 0
@@ -71,7 +55,7 @@ async def _safe_get(
) -> object | None:
"""Send a command and return *default* if it fails."""
try:
return _ok(await client.send(command))
return ok(await client.send(command))
except (
Fail2BanConnectionError,
Fail2BanProtocolError,