Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ jobs:
pip install --quiet -r requirements.txt
pip install --quiet pytest httpx

- name: Lint
# Same pinned ruff and policy as Pi (ruff.toml).
run: |
pip install --quiet ruff==0.16.6
python -m ruff check .

- name: The module contract requires these files to exist
run: |
missing=""
Expand Down
34 changes: 34 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Lint settings for SystemGate; the same policy as Pi's, shared across Conker modules.
#
# Explicit rather than default, because the defaults move between ruff releases
# and a lint set that changes under you produces diffs nobody asked for. Pinned
# in requirements-dev.txt for the same reason.

line-length = 100
target-version = "py311"

[lint]
select = [
"E", "W", # pycodestyle
"F", # pyflakes
"I", # import order
"UP", # modern syntax for the target version
"B", # bugbear
"SIM", # obvious simplifications
"ISC", # implicit string concatenation, which hides missing commas
"RUF",
]
ignore = [
# Health checks and status endpoints catch broadly on purpose. This service
# reports `unavailable` with a reason when a dependency misbehaves; a check
# that raised instead would take down the very endpoint the owner uses to
# find out what is wrong. See the truthful-status rule in CLAUDE.md.
"BLE001",
# Line length is left to review until this module adopts `ruff format`.
"E501",
]

[lint.per-file-ignores]
# Test doubles subclass stdlib handlers whose method names are fixed by the
# stdlib, and hold shared state as class attributes on purpose.
"tests/*" = ["N802", "RUF012", "F811"] # F811: pytest fixtures are imported, then requested by name
4 changes: 2 additions & 2 deletions systemgate/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def verify_snapshot(directory: Path) -> dict:
except (KeyError, TypeError, AttributeError, ValueError, OverflowError):
_invalid("Snapshot creation time is invalid; verify the manifest and system clock.")
files = manifest.get("files")
if not isinstance(files, dict) or not REQUIRED <= files.keys():
if not isinstance(files, dict) or not files.keys() >= REQUIRED:
_invalid("Required files are missing from the manifest; create a complete backup.")
actual = _files(directory)
if files.keys() != actual.keys():
Expand All @@ -126,7 +126,7 @@ def verify_snapshot(directory: Path) -> dict:
for image in images.values())):
_invalid("Image identities are invalid; obtain an intact version manifest.")
stores = manifest.get("stores")
if (not isinstance(stores, dict) or not STORES <= stores.keys()
if (not isinstance(stores, dict) or not stores.keys() >= STORES
or any(name not in STORES and not re.fullmatch(r"extra-[a-z]+-[0-9]+", name)
for name in stores)
or {name + ".tar" for name in stores} != {n for n in files if n.endswith(".tar")}):
Expand Down
6 changes: 3 additions & 3 deletions systemgate/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import subprocess
import time
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -115,7 +115,7 @@ def health(request: Request):
scanning, and so one dashboard renders every module with no special cases.
"""
settings = request.app.state.settings
checked_at = datetime.now(timezone.utc)
checked_at = datetime.now(UTC)
checks = {
"procfs": _probe(psutil.virtual_memory),
"docker": _probe(lambda: _docker_client().ping()),
Expand All @@ -139,7 +139,7 @@ def health(request: Request):
def vitals():
temps = {}
try:
sensors = getattr(psutil, "sensors_temperatures")
sensors = psutil.sensors_temperatures
temps = {name: [entry._asdict() for entry in values] for name, values in sensors(fahrenheit=False).items()}
except (AttributeError, OSError):
temps = {}
Expand Down
18 changes: 9 additions & 9 deletions systemgate/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import math
import socket
import time
from datetime import datetime, timezone
from datetime import UTC, datetime

MAX_ROWS = 200
MAX_SCAN = 2000
Expand Down Expand Up @@ -92,9 +92,9 @@ def add(section, row):
):
break
process_ids[pid] = (created, identity)
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("processes", "process_unavailable")
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("processes", "collection_failed", True)

try:
Expand All @@ -119,7 +119,7 @@ def add(section, row):
created = float(psutil.Process(conn.pid).create_time())
if created == process_ids[conn.pid][0]:
process_id = process_ids[conn.pid][1]
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("ports", "process_link_unavailable")
key = f"{protocol}:{address}:{port}:{process_id or 'unknown'}"
if not add(
Expand All @@ -138,7 +138,7 @@ def add(section, row):
},
):
break
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("ports", "listener_collection_failed", True)

client = None
Expand Down Expand Up @@ -207,22 +207,22 @@ def add(section, row):
},
):
break
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("containers", "container_unavailable")
error("ports", "container_bindings_unavailable")
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("containers", "collection_failed", True)
error("ports", "container_bindings_unavailable", True)
finally:
if client is not None:
try:
client.close()
except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque.
except Exception: # Collector failures must remain isolated and opaque.
error("containers", "client_close_failed")
finished = clock()
return {
"mode": "observed",
"sampledAt": datetime.fromtimestamp(started, timezone.utc).isoformat(),
"sampledAt": datetime.fromtimestamp(started, UTC).isoformat(),
"ageSeconds": max(0, finished - started),
"collectionSeconds": max(0, finished - started),
"source": {
Expand Down
1 change: 0 additions & 1 deletion tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from unittest.mock import Mock

import pytest

from fastapi.testclient import TestClient


Expand Down
Loading