refactor: improve backend type safety and import organization

- Add TYPE_CHECKING guards for runtime-expensive imports (aiohttp, aiosqlite)
- Reorganize imports to follow PEP 8 conventions
- Convert TypeAlias to modern PEP 695 type syntax (where appropriate)
- Use Sequence/Mapping from collections.abc for type hints (covariant)
- Replace string literals with cast() for improved type inference
- Fix casting of Fail2BanResponse and TypedDict patterns
- Add IpLookupResult TypedDict for precise return type annotation
- Reformat overlong lines for readability (120 char limit)
- Add asyncio_mode and filterwarnings to pytest config
- Update test fixtures with improved type hints

This improves mypy type checking and makes type relationships explicit.
This commit is contained in:
2026-03-20 13:44:14 +01:00
parent 6515164d53
commit 250bb1a2e5
30 changed files with 431 additions and 644 deletions

View File

@@ -14,17 +14,11 @@ import asyncio
import json
import time
from collections.abc import Awaitable, Callable
from dataclasses import asdict
from datetime import UTC, datetime
from typing import TYPE_CHECKING, TypeAlias
from typing import TYPE_CHECKING, cast
import structlog
if TYPE_CHECKING:
import aiosqlite
from app.services.geo_service import GeoInfo
from app.models.ban import (
BLOCKLIST_JAIL,
BUCKET_SECONDS,
@@ -37,20 +31,25 @@ from app.models.ban import (
BanTrendResponse,
DashboardBanItem,
DashboardBanListResponse,
JailBanCount as JailBanCountModel,
TimeRange,
_derive_origin,
bucket_count,
)
from app.models.ban import (
JailBanCount as JailBanCountModel,
)
from app.repositories import fail2ban_db_repo
from app.utils.fail2ban_client import Fail2BanClient
from app.utils.fail2ban_client import Fail2BanClient, Fail2BanResponse
if TYPE_CHECKING:
import aiohttp
import aiosqlite
from app.services.geo_service import GeoInfo
log: structlog.stdlib.BoundLogger = structlog.get_logger()
GeoEnricher: TypeAlias = Callable[[str], Awaitable["GeoInfo"] | None]
type GeoEnricher = Callable[[str], Awaitable[GeoInfo | None]]
# ---------------------------------------------------------------------------
# Constants
@@ -137,7 +136,7 @@ async def _get_fail2ban_db_path(socket_path: str) -> str:
response = await client.send(["get", "dbfile"])
try:
code, data = response
code, data = cast("Fail2BanResponse", response)
except (TypeError, ValueError) as exc:
raise RuntimeError(f"Unexpected response from fail2ban: {response!r}") from exc
@@ -276,7 +275,7 @@ async def list_bans(
# Batch-resolve geo data for all IPs on this page in a single API call.
# This avoids hitting the 45 req/min single-IP rate limit when the
# page contains many bans (e.g. after a large blocklist import).
geo_map: dict[str, "GeoInfo"] = {}
geo_map: dict[str, GeoInfo] = {}
if http_session is not None and rows:
page_ips: list[str] = [r.ip for r in rows]
try:
@@ -428,7 +427,7 @@ async def bans_by_country(
)
unique_ips: list[str] = [r.ip for r in agg_rows]
geo_map: dict[str, "GeoInfo"] = {}
geo_map: dict[str, GeoInfo] = {}
if http_session is not None and unique_ips:
# Serve only what is already in the in-memory cache — no API calls on
@@ -449,7 +448,7 @@ async def bans_by_country(
)
elif geo_enricher is not None and unique_ips:
# Fallback: legacy per-IP enricher (used in tests / older callers).
async def _safe_lookup(ip: str) -> tuple[str, "GeoInfo" | None]:
async def _safe_lookup(ip: str) -> tuple[str, GeoInfo | None]:
try:
return ip, await geo_enricher(ip)
except Exception: # noqa: BLE001
@@ -636,9 +635,7 @@ async def bans_by_jail(
# has *any* rows and log a warning with min/max timeofban so operators can
# diagnose timezone or filter mismatches from logs.
if total == 0:
table_row_count, min_timeofban, max_timeofban = (
await fail2ban_db_repo.get_bans_table_summary(db_path)
)
table_row_count, min_timeofban, max_timeofban = await fail2ban_db_repo.get_bans_table_summary(db_path)
if table_row_count > 0:
log.warning(
"ban_service_bans_by_jail_empty_despite_data",