fix: annotate to mypy-clean + promote to gating (1.0.0-40)
Five files needed annotation tweaks to clear the 14 outstanding mypy errors, all cosmetic (zero runtime bugs): - settings_store._coerce: return Any (concrete type depends on key, no narrowing path mypy can follow from the dict lookup) - retention._state: explicit dict[str, str | None] init - mailer: explicit `server: smtplib.SMTP` binding so SMTP_SSL and SMTP both narrow to the parent class for shared call sites - burnin/stages.py: TypedDict for the badblocks result dict so `result["bad_blocks"]` narrows to int at the comparison site scripts/security-scan.sh: mypy now counted in TOTAL_EXIT and findings.log line. Comment updated to reflect gating status.
This commit is contained in:
parent
cd92a4d3c8
commit
7cd66d460f
6 changed files with 34 additions and 13 deletions
|
|
@ -14,9 +14,16 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import TypedDict
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class _BadblocksResult(TypedDict):
|
||||
bad_blocks: int
|
||||
output: str
|
||||
aborted: bool
|
||||
|
||||
from . import kill
|
||||
from ._common import (
|
||||
POLL_INTERVAL,
|
||||
|
|
@ -385,7 +392,7 @@ async def _stage_surface_validate_ssh(job_id: int, devname: str, drive_id: int)
|
|||
# Streaming + progress is handled by the inline _drain coroutines
|
||||
# below; the in-loop _append_stage_log + _update_stage_percent calls
|
||||
# take care of throttled DB writes. Result dict is just final tallies.
|
||||
result = {"bad_blocks": 0, "output": "", "aborted": False}
|
||||
result: _BadblocksResult = {"bad_blocks": 0, "output": "", "aborted": False}
|
||||
try:
|
||||
bad_blocks_total = 0
|
||||
output_lines: list[str] = []
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class Settings(BaseSettings):
|
|||
ssh_key: str = "" # PEM private key content (paste full key including headers)
|
||||
|
||||
# Application version — used by the /api/v1/updates/check endpoint
|
||||
app_version: str = "1.0.0-39"
|
||||
app_version: str = "1.0.0-40"
|
||||
|
||||
# ---- Authentication (1.0.0-22) ----
|
||||
# session_secret: HMAC key for signing session cookies. Empty = generate
|
||||
|
|
|
|||
|
|
@ -303,6 +303,9 @@ def _send_email(subject: str, html: str) -> None:
|
|||
timeout = int(settings.smtp_timeout or 60)
|
||||
port = _smtp_port()
|
||||
|
||||
# SMTP / SMTP_SSL share a parent class but mypy can't unify them
|
||||
# without an explicit Union annotation on the binding.
|
||||
server: smtplib.SMTP
|
||||
if mode == "ssl":
|
||||
server = smtplib.SMTP_SSL(settings.smtp_host, port, context=ctx, timeout=timeout)
|
||||
server.ehlo()
|
||||
|
|
@ -465,6 +468,7 @@ async def test_smtp_connection() -> dict:
|
|||
timeout = int(settings.smtp_timeout or 60)
|
||||
port = _smtp_port()
|
||||
|
||||
server: smtplib.SMTP
|
||||
if mode == "ssl":
|
||||
server = smtplib.SMTP_SSL(settings.smtp_host, port,
|
||||
context=ctx, timeout=timeout)
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ async def backup_db(keep_count: int) -> Path | None:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RUN_HOUR = 3 # 03:00 local time — quiet for most homelabs
|
||||
_state = {"last_run_date": None}
|
||||
_state: dict[str, str | None] = {"last_run_date": None}
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ a container restart to fully take effect (clients/middleware are initialized at
|
|||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
|
@ -65,7 +66,14 @@ def _overrides_path() -> Path:
|
|||
return Path(settings.db_path).parent / "settings_overrides.json"
|
||||
|
||||
|
||||
def _coerce(key: str, raw) -> object:
|
||||
def _coerce(key: str, raw: Any) -> Any:
|
||||
"""Coerce a raw value to the type registered in _EDITABLE.
|
||||
|
||||
Return type is Any because the concrete return type depends on
|
||||
the key — int/str/bool — and there's no narrowing path mypy can
|
||||
follow from the dict lookup. Callers know which type to expect
|
||||
based on the field they're reading.
|
||||
"""
|
||||
coerce = _EDITABLE[key]
|
||||
if coerce is bool:
|
||||
if isinstance(raw, bool):
|
||||
|
|
|
|||
|
|
@ -87,17 +87,18 @@ docker run --rm \
|
|||
BANDITS=$?
|
||||
echo " exit=$BANDITS ($OUT_DIR/bandit.txt)" | tee -a "$OUT_DIR/summary.txt"
|
||||
|
||||
# --- mypy against the deploy dir (informational only) -------------------
|
||||
# --- mypy against the deploy dir (gating as of 1.0.0-40) ----------------
|
||||
# Type checker — surfaces None-handling bugs and missing-attribute errors
|
||||
# the runtime would have caught at the worst possible moment. Doesn't
|
||||
# count toward the failure exit-code sum until the codebase is annotated
|
||||
# enough to make findings actionable.
|
||||
# the runtime would have caught at the worst possible moment.
|
||||
#
|
||||
# Mount at /opt/app/app so internal `from . import X` resolves through
|
||||
# the `app` package (not `src`). Without this the relative imports inside
|
||||
# subpackages like burnin/ produce spurious "Module 'src' has no
|
||||
# attribute 'X'" errors that look like real bugs but are scan-env noise.
|
||||
echo "--- mypy (informational) ---" | tee -a "$OUT_DIR/summary.txt"
|
||||
#
|
||||
# Now counted toward TOTAL_EXIT — the codebase is fully clean under
|
||||
# `--ignore-missing-imports --no-strict-optional`. New errors fail the scan.
|
||||
echo "--- mypy ---" | tee -a "$OUT_DIR/summary.txt"
|
||||
docker run --rm \
|
||||
-v "$DEPLOY_DIR/app:/opt/app/app:ro" \
|
||||
-w /opt/app \
|
||||
|
|
@ -105,7 +106,7 @@ docker run --rm \
|
|||
"pip install --quiet --no-cache-dir --disable-pip-version-check mypy 2>&1 | tail -3 && mypy --ignore-missing-imports --no-strict-optional app" \
|
||||
> "$OUT_DIR/mypy.txt" 2>&1
|
||||
MYPY=$?
|
||||
echo " exit=$MYPY ($OUT_DIR/mypy.txt) — informational only" | tee -a "$OUT_DIR/summary.txt"
|
||||
echo " exit=$MYPY ($OUT_DIR/mypy.txt)" | tee -a "$OUT_DIR/summary.txt"
|
||||
|
||||
# --- gitleaks against the full git history ------------------------------
|
||||
echo "--- gitleaks ---" | tee -a "$OUT_DIR/summary.txt"
|
||||
|
|
@ -118,18 +119,19 @@ LEAKS=$?
|
|||
echo " exit=$LEAKS ($OUT_DIR/gitleaks.txt)" | tee -a "$OUT_DIR/summary.txt"
|
||||
|
||||
# --- summary + notification --------------------------------------------
|
||||
TOTAL_EXIT=$(( PIPS + BANDITS + LEAKS ))
|
||||
TOTAL_EXIT=$(( PIPS + BANDITS + MYPY + LEAKS ))
|
||||
{
|
||||
echo
|
||||
echo "Total findings exit-code sum: $TOTAL_EXIT"
|
||||
echo " pip-audit: $PIPS"
|
||||
echo " bandit: $BANDITS"
|
||||
echo " mypy: $MYPY"
|
||||
echo " gitleaks: $LEAKS"
|
||||
} >> "$OUT_DIR/summary.txt"
|
||||
|
||||
if [ "$TOTAL_EXIT" -ne 0 ]; then
|
||||
printf '%s — findings (pip-audit=%d bandit=%d gitleaks=%d) — see %s\n' \
|
||||
"$DATE" "$PIPS" "$BANDITS" "$LEAKS" "$OUT_DIR" >> "$SUMMARY"
|
||||
printf '%s — findings (pip-audit=%d bandit=%d mypy=%d gitleaks=%d) — see %s\n' \
|
||||
"$DATE" "$PIPS" "$BANDITS" "$MYPY" "$LEAKS" "$OUT_DIR" >> "$SUMMARY"
|
||||
# Hook for downstream notification — wire to your existing Mattermost
|
||||
# / Fastmail / webhook chain. Stays a no-op until SECURITY_SCAN_WEBHOOK
|
||||
# is set in the systemd unit's Environment=.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue