refactor(ban_service): extract _bans_by_country_load_data helper

Break up long function into focused helper. Load data logic separate from aggregation.
This commit is contained in:
2026-05-03 17:00:34 +02:00
parent 5058a50143
commit 2df029f7e8
8 changed files with 458 additions and 321 deletions

View File

@@ -39,6 +39,11 @@ See Backend-Development.md for the complete exception contract.
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.models.response import ErrorMetadata
# ---------------------------------------------------------------------------
# Exception Base Classes (Categories)
# ---------------------------------------------------------------------------
@@ -46,7 +51,7 @@ from __future__ import annotations
class DomainError(Exception):
"""Base class for all domain exceptions.
All domain exceptions must:
1. Define an `error_code` class attribute (machine-readable error code)
2. Implement `get_error_metadata()` to return structured error context
@@ -54,11 +59,11 @@ class DomainError(Exception):
error_code: str = "internal_error"
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
"""Return structured metadata for the API error response.
Subclasses should override to expose only safe, relevant metadata.
Returns:
A dictionary of metadata key-value pairs safe for client consumption.
"""
@@ -116,7 +121,7 @@ class RateLimitError(DomainError):
self.retry_after_seconds: float = retry_after_seconds
super().__init__(message)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"retry_after_seconds": self.retry_after_seconds}
@@ -134,7 +139,7 @@ class JailNotFoundError(NotFoundError):
self.name = name
super().__init__(f"Jail not found: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"jail_name": self.name}
@@ -176,7 +181,7 @@ class ConfigFileNotFoundError(NotFoundError):
self.filename = filename
super().__init__(f"Config file not found: {filename!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"filename": self.filename}
@@ -194,7 +199,7 @@ class ConfigFileExistsError(ConflictError):
self.filename = filename
super().__init__(f"Config file already exists: {filename!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"filename": self.filename}
@@ -231,7 +236,7 @@ class Fail2BanConnectionError(ServiceUnavailableError):
self.socket_path: str = socket_path
super().__init__(f"{message} (socket: {socket_path})")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"socket_path": self.socket_path}
@@ -252,7 +257,7 @@ class FilterInvalidRegexError(BadRequestError):
self.error = error
super().__init__(f"Invalid regex {pattern!r}: {error}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"pattern": self.pattern, "error": self.error}
@@ -276,7 +281,7 @@ class FilterRegexTooLongError(BadRequestError):
f"{self.actual_length} provided"
)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {
"pattern_length": self.actual_length,
"max_length": self.max_length,
@@ -302,7 +307,7 @@ class FilterRegexTimeoutError(BadRequestError):
f"(possible ReDoS attack). Pattern is too complex or causes catastrophic backtracking."
)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"timeout_seconds": self.timeout_seconds}
@@ -315,7 +320,7 @@ class JailNotFoundInConfigError(NotFoundError):
self.name = name
super().__init__(f"Jail not found in config: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"jail_name": self.name}
@@ -328,7 +333,7 @@ class ConfigWriteError(OperationError):
self.message = message
super().__init__(message)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"message": self.message}
@@ -347,7 +352,7 @@ class JailAlreadyActiveError(ConflictError):
self.name = name
super().__init__(f"Jail is already active: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"jail_name": self.name}
@@ -360,7 +365,7 @@ class JailAlreadyInactiveError(ConflictError):
self.name = name
super().__init__(f"Jail is already inactive: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"jail_name": self.name}
@@ -373,7 +378,7 @@ class FilterNotFoundError(NotFoundError):
self.name = name
super().__init__(f"Filter not found: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"filter_name": self.name}
@@ -386,7 +391,7 @@ class FilterAlreadyExistsError(ConflictError):
self.name = name
super().__init__(f"Filter already exists: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"filter_name": self.name}
@@ -407,7 +412,7 @@ class FilterReadonlyError(ConflictError):
f"Filter {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"filter_name": self.name}
@@ -420,7 +425,7 @@ class ActionNotFoundError(NotFoundError):
self.name = name
super().__init__(f"Action not found: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"action_name": self.name}
@@ -433,7 +438,7 @@ class ActionAlreadyExistsError(ConflictError):
self.name = name
super().__init__(f"Action already exists: {name!r}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"action_name": self.name}
@@ -454,7 +459,7 @@ class ActionReadonlyError(ConflictError):
f"Action {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"action_name": self.name}
@@ -478,7 +483,7 @@ class BlocklistSourceNotFoundError(NotFoundError):
self.source_id = source_id
super().__init__(f"Blocklist source not found: {source_id}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"source_id": self.source_id}
@@ -494,7 +499,7 @@ class BlocklistSourceHasLogsError(ConflictError):
"Delete the import logs first."
)
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"source_id": self.source_id}
@@ -507,5 +512,5 @@ class HistoryNotFoundError(NotFoundError):
self.ip = ip
super().__init__(f"No history found for IP: {ip}")
def get_error_metadata(self) -> dict[str, str | int | float | bool | None]:
def get_error_metadata(self) -> ErrorMetadata:
return {"ip": self.ip}