fix(types): T7 — eliminate all remaining 38 mypy errors; fix DeckyRow subscript in engine tests
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -602,7 +602,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:
|
||||
@@ -659,7 +659,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'}",
|
||||
@@ -776,7 +776,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(
|
||||
@@ -813,7 +813,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:
|
||||
@@ -824,7 +824,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,
|
||||
),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -436,6 +436,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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -514,7 +514,7 @@ async def _materialise_decky_services_diff(
|
||||
break
|
||||
try:
|
||||
await anyio.to_thread.run_sync(
|
||||
lambda args=args: _compose(*args, *rm_targets, compose_file=compose_path),
|
||||
lambda args=args: _compose(*args, *rm_targets, compose_file=compose_path), # type: ignore[misc]
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_log.warning(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -308,7 +308,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -80,7 +80,7 @@ async def _get_attacker_uuid(repo: BaseRepository, ip: str) -> Optional[str]:
|
||||
from sqlalchemy import select
|
||||
async with repo._session() as session: # type: ignore[attr-defined]
|
||||
result = await session.execute(
|
||||
select(Attacker).where(Attacker.ip == ip)
|
||||
select(Attacker).where(Attacker.ip == ip) # type: ignore[arg-type]
|
||||
)
|
||||
row = result.scalar_one_or_none()
|
||||
return row.uuid if row else None
|
||||
|
||||
@@ -202,20 +202,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -226,7 +226,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(
|
||||
@@ -267,7 +267,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -114,7 +114,7 @@ def _get_active_connections(pid: int, ports: list[int]) -> list[dict]:
|
||||
)
|
||||
async def api_enable_tarpit(
|
||||
decky_name: str = Path(..., pattern=_DECKY_RE),
|
||||
req: TarpitEnableRequest = ...,
|
||||
req: TarpitEnableRequest = ..., # type: ignore[assignment]
|
||||
admin: dict = Depends(require_admin),
|
||||
) -> MessageResponse:
|
||||
try:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -59,7 +59,7 @@ def _db_key(topology_id: str, decky_name: str) -> str:
|
||||
async def api_enable_tarpit(
|
||||
topology_id: str = Path(..., pattern=_TOPO_RE),
|
||||
decky_name: str = Path(..., pattern=_DECKY_RE),
|
||||
req: TarpitEnableRequest = ...,
|
||||
req: TarpitEnableRequest = ..., # type: ignore[assignment]
|
||||
admin: dict = Depends(require_admin),
|
||||
) -> MessageResponse:
|
||||
try:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -80,8 +80,8 @@ async def test_update_persists_validated_cfg_no_recreate_on_save(
|
||||
rows = await repo.list_topology_deckies(
|
||||
topology_with_ssh_decky["topology_id"]
|
||||
)
|
||||
row = next(r for r in rows if r["uuid"] == topology_with_ssh_decky["decky_uuid"])
|
||||
cfg_blob = row["decky_config"]
|
||||
row = next(r for r in rows if r.uuid == topology_with_ssh_decky["decky_uuid"])
|
||||
cfg_blob = row.decky_config
|
||||
if isinstance(cfg_blob, str):
|
||||
cfg_blob = json.loads(cfg_blob)
|
||||
assert cfg_blob["service_config"]["ssh"] == {"password": "hunter2"}
|
||||
|
||||
@@ -18,9 +18,9 @@ async def _get_topology_decky(repo, decky_uuid: str) -> dict[str, Any]:
|
||||
# Iterate all topologies' deckies — fine for tests with one row.
|
||||
topologies = await repo.list_topologies()
|
||||
for t in topologies:
|
||||
for d in await repo.list_topology_deckies(t["id"]):
|
||||
if d.get("uuid") == decky_uuid:
|
||||
return d
|
||||
for d in await repo.list_topology_deckies(t.id):
|
||||
if d.uuid == decky_uuid:
|
||||
return d.model_dump()
|
||||
raise AssertionError(f"decky {decky_uuid!r} not found in any topology")
|
||||
|
||||
from decnet.bus.fake import FakeBus
|
||||
|
||||
Reference in New Issue
Block a user