fix(types): T7 — eliminate all remaining 38 mypy errors; fix DeckyRow subscript in engine tests

This commit is contained in:
2026-05-01 02:07:53 -04:00
parent bd50b0d8b2
commit 776861a1b7
21 changed files with 49 additions and 41 deletions

View File

@@ -194,7 +194,7 @@ async def self_destruct() -> None:
argv = ["/bin/bash", path]
spawn_kwargs = {"start_new_session": True}
subprocess.Popen( # nosec B603
subprocess.Popen( # type: ignore[call-overload] # nosec B603
argv,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,

View File

@@ -121,7 +121,7 @@ def start() -> Optional[asyncio.Task]:
return None
try:
from decnet import __version__ as _v
from decnet import __version__ as _v # type: ignore[attr-defined]
agent_version = _v
except Exception:
agent_version = "unknown"

View File

@@ -58,7 +58,7 @@ def make_thread_safe_publisher(
contract the rest of this module already upholds.
"""
if bus is None:
return lambda _topic, _payload, _event_type="": None
return lambda _topic, _payload, _event_type="": None # type: ignore[misc]
def _publish(topic: str, payload: dict[str, Any], event_type: str = "") -> None:
# Stream threads may keep draining after the bus owner closed it

View File

@@ -131,7 +131,7 @@ def _build_response(
question = qname_bytes + struct.pack("!HH", query.qtype, query.qclass)
answer = b""
if an_count:
if an_count and answer_ip is not None:
# Use a name pointer back to the question (offset 12).
ptr = struct.pack("!H", 0xC000 | 12)
rdata = bytes(int(o) for o in answer_ip.split("."))
@@ -190,7 +190,7 @@ class CanaryDNSProtocol(asyncio.DatagramProtocol):
return
# Known name — answer with our sinkhole IP, then fire the hook.
self._send(addr, _build_response(query, answer_ip=self._answer_ip))
asyncio.create_task(self._hook(slug, query, addr[0]))
asyncio.ensure_future(self._hook(slug, query, addr[0]))
def _slug_for(self, qname: str) -> Optional[str]:
if not self._zone or not qname.endswith(self._suffix):

View File

@@ -65,7 +65,7 @@ def _gate_commands_by_mode(_app: typer.Typer) -> None:
return
_app.registered_commands = [
c for c in _app.registered_commands
if (c.name or c.callback.__name__) not in MASTER_ONLY_COMMANDS
if (c.name or (c.callback.__name__ if c.callback else "")) not in MASTER_ONLY_COMMANDS
]
_app.registered_groups = [
g for g in _app.registered_groups

View File

@@ -601,7 +601,7 @@ def register(app: typer.Typer) -> None:
# (Path("/"). / "/opt/decnet" == Path("/opt/decnet"), dropping pfx).
_install_rel = install_dir.lstrip("/")
required_tools = ("systemctl",) if deinit else (
required_tools: tuple[str, ...] = ("systemctl",) if deinit else (
"systemctl", "useradd", "groupadd", "systemd-tmpfiles",
)
if deinit:
@@ -658,7 +658,7 @@ def register(app: typer.Typer) -> None:
)
_step(
"systemctl daemon-reload",
lambda: (_run(["systemctl", "daemon-reload"], dry_run=dry_run), "ok")[1],
lambda: (_run(["systemctl", "daemon-reload"], dry_run=dry_run), "ok")[1], # type: ignore[func-returns-value]
)
_step(
f"remove {etc_decnet / 'decnet.ini'}",
@@ -775,7 +775,7 @@ def register(app: typer.Typer) -> None:
for path, mode, d_owner, d_group in dirs:
_step(
f"ensure dir {path}",
lambda p=path, m=mode, o=d_owner, g=d_group:
lambda p=path, m=mode, o=d_owner, g=d_group: # type: ignore[misc]
_ensure_dir(p, mode=m, owner=o, group=g, dry_run=dry_run),
)
_step(
@@ -812,7 +812,7 @@ def register(app: typer.Typer) -> None:
)
_step(
"systemctl daemon-reload",
lambda: (_run(["systemctl", "daemon-reload"], dry_run=dry_run), "ok")[1],
lambda: (_run(["systemctl", "daemon-reload"], dry_run=dry_run), "ok")[1], # type: ignore[func-returns-value]
)
if no_start:
@@ -823,7 +823,7 @@ def register(app: typer.Typer) -> None:
_step(
"systemctl enable --now decnet.target",
lambda: (
_run(
_run( # type: ignore[func-returns-value]
["systemctl", "enable", "--now", "decnet.target"],
dry_run=dry_run,
),

View File

@@ -11,7 +11,7 @@ import signal
import subprocess # nosec B404
import sys
from pathlib import Path
from typing import Optional
from typing import Any, Callable, Optional
import typer
from rich.console import Console
@@ -96,7 +96,7 @@ def _is_running(match_fn) -> int | None:
return None
def _service_registry(log_file: str) -> list[tuple[str, callable, list[str]]]:
def _service_registry(log_file: str) -> list[tuple[str, Callable[..., Any], list[str]]]:
"""Return the microservice registry for health-check and relaunch.
On agents these run as systemd units invoking /usr/local/bin/decnet,
@@ -195,7 +195,7 @@ _DEFAULT_SWARMCTL_URL = "http://127.0.0.1:8770"
def _swarmctl_base_url(url: Optional[str]) -> str:
return url or os.environ.get("DECNET_SWARMCTL_URL", _DEFAULT_SWARMCTL_URL)
return url or os.environ.get("DECNET_SWARMCTL_URL") or _DEFAULT_SWARMCTL_URL
def _http_request(method: str, url: str, *, json_body: Optional[dict] = None, timeout: float = 30.0):

View File

@@ -393,6 +393,8 @@ def _compose_with_retry(
console.print(f"[red]{result.stderr.strip()}[/]")
log.error("docker compose %s failed after %d attempts: %s",
" ".join(args), retries, result.stderr.strip())
if last_exc is None: # pragma: no cover — retries=0 is not a supported call
raise RuntimeError("_compose_with_retry exhausted retries without capturing an error")
raise last_exc

View File

@@ -101,7 +101,10 @@ async def mutate_decky(
try:
# Wrap blocking call in thread
await anyio.to_thread.run_sync(_compose_with_retry, "up", "-d", "--remove-orphans", compose_path)
cp = compose_path
await anyio.to_thread.run_sync(
lambda: _compose_with_retry("up", "-d", "--remove-orphans", compose_file=cp)
)
except Exception as e:
log.error("mutation failed decky=%s error=%s", decky_name, e)
console.print(f"[red]Failed to mutate '{decky_name}': {e}[/]")
@@ -161,6 +164,8 @@ async def mutate_all(
if force or only is not None:
due = True
else:
if interval_mins is None:
continue
elapsed_secs = now - decky.last_mutated
due = elapsed_secs >= (interval_mins * 60)
remaining = (interval_mins * 60) - elapsed_secs

View File

@@ -65,7 +65,7 @@ def get_driver_for(action: Action) -> ActivityDriver:
try:
from decnet.orchestrator.emailgen.scheduler import EmailAction
except ImportError: # pragma: no cover - scheduler always exists
EmailAction = None # type: ignore[assignment]
EmailAction = None # type: ignore[assignment, misc]
if EmailAction is not None and isinstance(action, EmailAction):
from decnet.orchestrator.drivers.email import EmailDriver
return EmailDriver()

View File

@@ -176,7 +176,7 @@ class EmailDriver(ActivityDriver):
"""Convenience accessor for telemetry / logging."""
return self._llm.model
async def run(self, action: EmailAction) -> ActivityResult:
async def run(self, action: EmailAction) -> ActivityResult: # type: ignore[override]
return await self._run_email(action)
async def _run_email(self, action: EmailAction) -> ActivityResult:

View File

@@ -303,7 +303,7 @@ async def _pick_action(
)
elif kind == "email":
try:
action = await email_scheduler.pick(repo, rand=rng)
action = await email_scheduler.pick(repo, rand=rng) # type: ignore[assignment]
except Exception as exc: # noqa: BLE001
logger.debug("orchestrator: email pick failed: %s", exc)
action = None

View File

@@ -229,7 +229,7 @@ def issue_worker_cert(
)
.add_extension(x509.SubjectAlternativeName(san_entries), critical=False)
)
cert = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
cert = builder.sign(private_key=ca_key, algorithm=hashes.SHA256()) # type: ignore[arg-type]
cert_pem = _pem_cert(cert)
fp = hashlib.sha256(
cert.public_bytes(serialization.Encoding.DER)

View File

@@ -195,20 +195,20 @@ def _backfill_decky_configs(
alloc = _alloc(lan_id)
if alloc is None:
continue
ip: str | None = None
assigned_ip: str | None = None
if primary_ip:
try:
if (
IPv4Address(primary_ip) in IPv4Network(lan["subnet"])
and alloc.is_free(primary_ip)
):
ip = primary_ip
alloc.reserve(ip)
assigned_ip = primary_ip
alloc.reserve(assigned_ip)
except (ValueError, TypeError):
pass
if ip is None:
ip = alloc.next_free()
ips_by_lan[lan["name"]] = ip
if assigned_ip is None:
assigned_ip = alloc.next_free()
ips_by_lan[lan["name"]] = assigned_ip
cfg["ips_by_lan"] = ips_by_lan
decky["decky_config"] = cfg

View File

@@ -11,7 +11,7 @@ from typing import Any, AsyncGenerator, Optional
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import ORJSONResponse
from fastapi.responses import ORJSONResponse, Response
from pydantic import ValidationError
from fastapi.middleware.cors import CORSMiddleware
@@ -218,7 +218,7 @@ app: FastAPI = FastAPI(
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore[arg-type]
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(
@@ -259,7 +259,7 @@ app.include_router(api_router, prefix="/api/v1")
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> ORJSONResponse:
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> Response:
"""
Handle validation errors with targeted status codes to satisfy contract tests.
Tiered Prioritization:

View File

@@ -19,6 +19,7 @@ def get_repository(**kwargs: Any) -> BaseRepository:
* MySQL accepts ``url`` and engine tuning knobs (``pool_size``, …).
"""
db_type = os.environ.get("DECNET_DB_TYPE", "sqlite").lower()
repo: BaseRepository
if db_type == "sqlite":
from decnet.web.db.sqlite.repository import SQLiteRepository

View File

@@ -12,11 +12,11 @@ SQLite's:
"""
from __future__ import annotations
from typing import List, Optional
from typing import Any, List, Optional
from sqlalchemy import func, select, text, literal_column
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from sqlmodel.sql.expression import SelectOfScalar
from decnet.web.db.models import Log
from decnet.web.db.mysql.database import get_async_engine
@@ -162,11 +162,11 @@ class MySQLRepository(SQLModelRepository):
# Truncate each timestamp to the start of its bucket:
# FROM_UNIXTIME( (UNIX_TIMESTAMP(timestamp) DIV N) * N )
# DIV is MySQL's integer division operator.
bucket_expr = literal_column(
bucket_expr: Any = literal_column(
f"FROM_UNIXTIME((UNIX_TIMESTAMP(timestamp) DIV {bucket_seconds}) * {bucket_seconds})"
).label("bucket_time")
statement: SelectOfScalar = select(bucket_expr, func.count().label("count")).select_from(Log)
statement: Any = select(bucket_expr, func.count().label("count")).select_from(Log)
statement = self._apply_filters(statement, search, start_time, end_time)
statement = statement.group_by(literal_column("bucket_time")).order_by(
literal_column("bucket_time")

View File

@@ -1,8 +1,8 @@
from typing import List, Optional
from typing import Any, List, Optional
from sqlalchemy import func, select, text, literal_column
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from sqlmodel.sql.expression import SelectOfScalar
from decnet.config import _ROOT
from decnet.web.db.models import Log
@@ -91,11 +91,11 @@ class SQLiteRepository(SQLModelRepository):
interval_minutes: int = 15,
) -> List[dict]:
bucket_seconds = max(interval_minutes, 1) * 60
bucket_expr = literal_column(
bucket_expr: Any = literal_column(
f"datetime((strftime('%s', timestamp) / {bucket_seconds}) * {bucket_seconds}, 'unixepoch')"
).label("bucket_time")
statement: SelectOfScalar = select(bucket_expr, func.count().label("count")).select_from(Log)
statement: Any = select(bucket_expr, func.count().label("count")).select_from(Log)
statement = self._apply_filters(statement, search, start_time, end_time)
statement = statement.group_by(literal_column("bucket_time")).order_by(
literal_column("bucket_time")

View File

@@ -39,7 +39,7 @@ router = APIRouter()
},
)
@limiter.limit("10/5 minutes", key_func=login_ip_key)
@limiter.limit("10/5 minutes", key_func=login_username_key)
@limiter.limit("10/5 minutes", key_func=login_username_key) # type: ignore[arg-type]
@_traced("api.login")
async def login(request: Request, payload: LoginRequest) -> dict[str, Any]:
_user: Optional[dict[str, Any]] = await get_user_by_username_cached(payload.username)

View File

@@ -108,7 +108,7 @@ async def get_health(user: dict = Depends(require_viewer)) -> Any:
if _docker_client is None:
_docker_client = await asyncio.to_thread(docker.from_env)
await asyncio.to_thread(_docker_client.ping)
await asyncio.to_thread(_docker_client.ping) # type: ignore[union-attr]
_docker_healthy = True
_docker_detail = ""
except Exception as exc:

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import json
import secrets
from datetime import datetime, timezone
from typing import Any
from typing import Any, cast
from fastapi import APIRouter, Depends, HTTPException
@@ -66,7 +66,7 @@ async def api_create_webhook(
req: WebhookCreateRequest,
admin: dict = Depends(require_admin),
) -> WebhookCreateResponse:
patterns = merge_patterns(req.simple_events, req.topic_patterns)
patterns = merge_patterns(cast(list[str], req.simple_events), req.topic_patterns)
if not patterns:
raise HTTPException(
status_code=400,
@@ -188,7 +188,7 @@ async def api_update_webhook(
# to clear all patterns must explicitly pass both as empty lists.
simple = req.simple_events if req.simple_events is not None else []
raw = req.topic_patterns if req.topic_patterns is not None else []
patterns = merge_patterns(simple, raw)
patterns = merge_patterns(cast(list[str], simple), raw)
if not patterns:
raise HTTPException(
status_code=400,