fix: clear all mypy errors

Annotate untyped containers and Optional attributes, type the stdlib-shaped
metric dicts in the client plugins, and add types-PyYAML/types-psutil to the
dev extras plus a mypy override for nio/mattermostdriver (no stubs).

Behaviour fixes found on the way: enable_kernel_timestamps() now returns False
when SO_TIMESTAMP is unknown instead of raising TypeError, send_shutdown is
defined at module level so cleanup() cannot hit a NameError, and a failed
subprocess returncode falls back to UNKNOWN.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 05:04:54 -04:00
co-authored by Claude Sonnet 5.5
parent 748349a7b9
commit 445ff6b474
20 changed files with 63 additions and 49 deletions
+3 -2
View File
@@ -2,15 +2,16 @@
import logging
import os
from typing import Any, Dict
logger = logging.getLogger(__name__)
try:
import yaml
except Exception:
yaml = None
yaml = None # type: ignore[assignment]
CLIENT_DEFAULTS = {
CLIENT_DEFAULTS: Dict[str, Any] = {
# Network settings
"hb_port": 50003, # Port where hbd servers listen
"interval": 10, # Heartbeat interval in seconds
+1
View File
@@ -36,6 +36,7 @@ MAXRECV = 32767
# Global state
running = True
dorestart = False
send_shutdown = False
shutdown_event: Optional[asyncio.Event] = None
active_tasks: List[asyncio.Task] = []
+2 -2
View File
@@ -17,7 +17,7 @@ import logging
import sys
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, List, Optional, Type
from typing import Any, Dict, List, Optional
class Plugin(ABC):
@@ -256,7 +256,7 @@ class PluginRegistry:
"""Get all enabled plugins."""
return [p for p in self._plugins.values() if p.enabled]
def get_by_type(self, plugin_type: Type[Plugin]) -> List[Plugin]:
def get_by_type(self, plugin_type: type) -> List[Plugin]:
"""Get all plugins of a specific type.
Args:
+1 -1
View File
@@ -28,7 +28,7 @@ class CPUMonitorPlugin(MonitorPlugin):
def __init__(self, config: Optional[Dict[str, Any]] = None):
super().__init__(config)
self.psutil = None
self.psutil: Any = None
self.per_core = config.get("per_core", False) if config else False
self.interval = config.get("interval", 300) if config else 300
+3 -3
View File
@@ -10,7 +10,7 @@ from typing import Dict, Any, Optional
try:
import psutil
except ImportError:
psutil = None
psutil = None # type: ignore[assignment]
from hbd.client.plugin import MonitorPlugin
@@ -58,7 +58,7 @@ class DiskMonitorPlugin(MonitorPlugin):
raise ImportError("psutil library is required for disk_monitor plugin")
# Store previous I/O counters for delta calculation
self._prev_io = {}
self._prev_io: Dict[str, Any] = {}
async def initialize(self):
"""Initialize the plugin (check psutil availability)."""
@@ -114,7 +114,7 @@ class DiskMonitorPlugin(MonitorPlugin):
async def _collect_metrics(self) -> Dict[str, Any]:
"""Collect disk metrics from psutil."""
metrics = {}
metrics: Dict[str, Any] = {}
# Collect partition usage
partitions_data = {}
+4 -4
View File
@@ -10,7 +10,7 @@ from typing import Dict, Any, Optional
try:
import psutil
except ImportError:
psutil = None
psutil = None # type: ignore[assignment]
from hbd.client.plugin import InfoPlugin
@@ -98,8 +98,8 @@ class FilesystemInfoPlugin(InfoPlugin):
async def _collect_info(self) -> Dict[str, Any]:
"""Collect filesystem information from psutil."""
info = {}
filesystems = []
info: Dict[str, Any] = {}
filesystems: list = []
filesystem_types = set()
# Get mounted disk partitions
@@ -112,7 +112,7 @@ class FilesystemInfoPlugin(InfoPlugin):
if partition.fstype in self.exclude_types:
continue
fs_info = {
fs_info: Dict[str, Any] = {
'device': partition.device,
'mountpoint': partition.mountpoint,
'fstype': partition.fstype,
+2 -2
View File
@@ -10,7 +10,7 @@ from typing import Dict, Any, Optional
try:
import psutil
except ImportError:
psutil = None
psutil = None # type: ignore[assignment]
from hbd.client.plugin import MonitorPlugin
@@ -116,7 +116,7 @@ class MemoryMonitorPlugin(MonitorPlugin):
async def _collect_metrics(self) -> Dict[str, Any]:
"""Collect memory metrics from psutil."""
metrics = {}
metrics: Dict[str, Any] = {}
# Virtual (physical) memory statistics
vmem = psutil.virtual_memory()
+4 -3
View File
@@ -124,7 +124,7 @@ class NagiosRunnerPlugin(MonitorPlugin):
Returns:
Dictionary with results from all plugins
"""
results = {}
results: Dict[str, Any] = {}
for cmd_config in self.commands:
name = cmd_config.get("name")
@@ -181,7 +181,8 @@ class NagiosRunnerPlugin(MonitorPlugin):
self.logger.error(f"Command timed out: {command}")
return NAGIOS_UNKNOWN, f"Command timed out after {self.timeout}s", {}
status_code = proc.returncode
# communicate() has finished, so the process has exited and returncode is set
status_code = proc.returncode if proc.returncode is not None else NAGIOS_UNKNOWN
if status_code < 0:
return NAGIOS_UNKNOWN, f"Process killed by signal {-status_code}", {}
@@ -225,7 +226,7 @@ class NagiosRunnerPlugin(MonitorPlugin):
Returns:
Dictionary of metric_name: value
"""
perfdata = {}
perfdata: Dict[str, Any] = {}
# Performance data comes after the pipe character
if '|' not in output:
+4 -4
View File
@@ -10,7 +10,7 @@ from typing import Dict, Any, Optional
try:
import psutil
except ImportError:
psutil = None
psutil = None # type: ignore[assignment]
from hbd.client.plugin import MonitorPlugin
from hbd.client.connectivity import get_shared
@@ -63,7 +63,7 @@ class NetworkMonitorPlugin(MonitorPlugin):
raise ImportError("psutil library is required for network_monitor plugin")
# Store previous I/O counters for delta calculation
self._prev_io = {}
self._prev_io: Dict[str, Any] = {}
async def initialize(self):
"""Initialize the plugin (check psutil availability)."""
@@ -124,7 +124,7 @@ class NetworkMonitorPlugin(MonitorPlugin):
async def _collect_metrics(self) -> Dict[str, Any]:
"""Collect network metrics from psutil."""
metrics = {}
metrics: Dict[str, Any] = {}
# Collect per-interface I/O counters
try:
@@ -169,7 +169,7 @@ class NetworkMonitorPlugin(MonitorPlugin):
if self.include_connections:
try:
connections = psutil.net_connections(kind='inet')
conn_stats = {}
conn_stats: Dict[str, int] = {}
# Count connections by state
for conn in connections:
+1
View File
@@ -67,6 +67,7 @@ class PingMonitorPlugin(MonitorPlugin):
self.timeout = int(cfg.get("timeout", 5))
# hosts: dict of {hostname: {warning: x, critical: y}} or list of hostnames
raw_hosts = cfg.get("hosts", {})
self.hosts: Dict[str, Any]
if isinstance(raw_hosts, list):
self.hosts = {h: {} for h in raw_hosts}
else:
+1 -1
View File
@@ -92,7 +92,7 @@ def stodict(msg: bytes):
'!' and decodes accordingly. Returns a dict with key 'ID' set to the
message ID and the parsed key/value pairs.
"""
d = {}
d: Dict[str, Any] = {}
if len(msg) > 0 and chr(msg[0]) == "!":
# message is: b'!ID:' + compressed_payload
# original code used msg[1:4].decode() for ID (3 bytes including colon)
+1 -1
View File
@@ -7,7 +7,7 @@ import os
try:
import yaml
except Exception:
yaml = None
yaml = None # type: ignore[assignment]
SERVER_DEFAULTS = {
# Network settings
+1 -1
View File
@@ -1,4 +1,4 @@
msgs = [] # in-memory list of recent messages for new websocket clients; also logged to file via notify.eventlog
msgs: list = [] # in-memory list of recent messages for new websocket clients; also logged to file via notify.eventlog
class Data:
+5 -5
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from subprocess import Popen, PIPE, STDOUT
from typing import Optional
from typing import Any, Callable, Optional
import asyncio
import logging
@@ -79,8 +79,8 @@ async def dns_update_worker(
hbdclass,
cfg: dict,
async_queue=None,
log: Optional[callable] = None,
pushmsg: Optional[callable] = None,
log: Optional[Callable[..., Any]] = None,
pushmsg: Optional[Callable[..., Any]] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
):
"""Pure async DNS worker that processes updates from asyncio.Queue.
@@ -164,7 +164,7 @@ async def dns_update_worker(
def start_dns_worker(
hbdclass,
cfg: dict,
log: Optional[callable] = None,
log: Optional[Callable[..., Any]] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
):
"""Start the async DNS worker and return the Task.
@@ -176,7 +176,7 @@ def start_dns_worker(
loop = asyncio.get_event_loop()
# Create asyncio.Queue and wrap in a bridge for thread-safe puts
async_q = asyncio.Queue()
async_q: asyncio.Queue[Any] = asyncio.Queue()
class _QueueBridge:
"""Thread-safe wrapper around asyncio.Queue for synchronous callers."""
+4 -3
View File
@@ -8,6 +8,7 @@ import time
import json
import copy
import queue
from typing import Any, Dict
num = 0
@@ -24,7 +25,7 @@ def log(host, m):
class Connection:
# map of addrs to names
htab = {}
htab: Dict[str, Any] = {}
UNKNOWN = "unknown"
UP = "up"
DOWN = "down"
@@ -278,8 +279,8 @@ class Connection:
#
class Host:
# Table of Hosts
hosts = {}
dnsQ = queue.Queue()
hosts: Dict[str, "Host"] = {}
dnsQ: "queue.Queue[Any]" = queue.Queue()
def __init__(self, name):
global num
+2 -2
View File
@@ -11,7 +11,7 @@ import os
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Optional, Callable, Iterable, List, Tuple, Union
from typing import Dict, Any, Optional, Callable, Iterable, List, TextIO, Tuple, Union
logger = logging.getLogger(__name__)
@@ -52,7 +52,7 @@ class MessageJournal:
self.enabled = self.config.get('journal_enabled', True)
# Runtime state
self._file_handle = None
self._file_handle: Optional[TextIO] = None
self._current_size = 0
self._lock = asyncio.Lock()
self._initialized = False
+1 -1
View File
@@ -128,7 +128,7 @@ def get_providers(config: dict) -> list[ResolvedProvider]:
Entries with missing required fields or unknown types are skipped with
a warning log. Order follows config declaration order.
"""
result = []
result: list = []
oauth_cfg = config.get("oauth", {})
if not isinstance(oauth_cfg, dict):
return result
+9 -9
View File
@@ -53,16 +53,16 @@ class AlertState:
self.metric_path = metric_path
self.level = AlertLevel.OK
self.since = time.time()
self.last_value = None
self.last_value: Any = None
self.last_check = time.time()
self.notification_count = 0
self.last_notification = None
self.threshold_value = None # The threshold value that triggered alert
self.operator = None # The comparison operator (>, <, >=, etc.)
self.last_notification: Optional[float] = None
self.threshold_value: Optional[float] = None # The threshold value that triggered alert
self.operator: Optional[str] = None # The comparison operator (>, <, >=, etc.)
self.hysteresis: Optional[float] = None # Hysteresis fraction used for recovery
self.formatted_message = None # Formatted display message for UI
self.formatted_message: Optional[str] = None # Formatted display message for UI
self.acknowledged = False # Whether alert has been acknowledged
self.acknowledged_at = None # Timestamp when acknowledged
self.acknowledged_at: Optional[float] = None # Timestamp when acknowledged
self.consecutive_count = 0 # Consecutive exceedances while still OK (for count gating)
self.pending_since: Optional[float] = None # non-None while waiting out grace period before notifying
@@ -362,13 +362,13 @@ class ThresholdChecker:
journal: Optional MessageJournal instance for logging threshold events
"""
# Named threshold configurations (pre-merged: defaults + overrides): {config_name: {metric_path: ThresholdConfig}}
self.threshold_configs = {}
self.threshold_configs: Dict[str, Dict[str, ThresholdConfig]] = {}
# Raw overrides only for each named config (no defaults baked in): {config_name: {metric_path: ThresholdConfig}}
self.threshold_raw_configs: Dict[str, Dict[str, ThresholdConfig]] = {}
# Single threshold set for backward compatibility: {metric_path: ThresholdConfig}
self.thresholds = {}
self.thresholds: Dict[str, ThresholdConfig] = {}
# Host to ordered list of config names: {host_name: [config_name, ...]}
self.host_config_mapping: Dict[str, List[str]] = {}
@@ -1276,7 +1276,7 @@ class ThresholdChecker:
def _format_display(
self,
display_format: str,
display_format: Optional[str],
value: Any,
threshold_value: Optional[float],
op_symbol: str,
+7 -5
View File
@@ -5,6 +5,7 @@ import socket
import struct
import time
import zlib
from typing import Optional
import logging
from platform import system as platform_system
@@ -25,6 +26,7 @@ _STALE_INTERVAL_MULTIPLIER = 3
# Supported on Linux, FreeBSD, and macOS. The constant is not exposed by
# Python's socket module on all platforms
platform = platform_system()
_SO_TIMESTAMP: Optional[int]
if platform == "Darwin":
_SO_TIMESTAMP = 1024 # SO_TIMESTAMP on macOS (not in Python's socket module)
elif platform == "Linux":
@@ -45,6 +47,8 @@ def enable_kernel_timestamps(sock) -> bool:
Returns True if the kernel will supply receive timestamps, False otherwise
(unsupported platform, older kernel, or insufficient permissions).
"""
if _SO_TIMESTAMP is None:
return False
try:
sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMP, 1)
return True
@@ -430,7 +434,7 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
return
cfg = ctx.get("config", {})
hbdcls = ctx.get("hbdclass")
hbdcls = ctx["hbdclass"]
msg_to_websockets = ctx.get("msg_to_websockets")
DEBUG = ctx.get("DEBUG", 0)
@@ -459,10 +463,8 @@ def handle_datagram(msg: dict, addr, transport, ctx: dict):
newh = False
cid = msg.get("id", 0)
try:
rtt = float(msg.get("rtt"))
except TypeError:
rtt = None
rtt_raw = msg.get("rtt")
rtt = float(rtt_raw) if rtt_raw is not None else None
if msg.get("ID") == "HTB":
host.doesack = msg.get("acks", -1)
+7
View File
@@ -62,6 +62,8 @@ dev = [
"pytest-cov>=4.0",
"flake8>=5.0",
"mypy>=1.10",
"types-PyYAML",
"types-psutil",
"black>=23.0",
"isort>=5.0",
"tox>=4.0",
@@ -92,5 +94,10 @@ max-line-length = 111
[tool.pylint.format]
max-line-length = 111
[[tool.mypy.overrides]]
# optional notification libraries that ship neither stubs nor py.typed
module = ["nio", "nio.*", "mattermostdriver", "mattermostdriver.*"]
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]