94 lines
2.9 KiB
Python
94 lines
2.9 KiB
Python
"""Read-only and transient preflight diagnostics for operators."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
import tempfile
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
from gyxx_flow.catalog import WorkflowCatalog
|
|
from gyxx_flow.core.config import Settings
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DiagnosticCheck:
|
|
name: str
|
|
passed: bool
|
|
message: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DoctorReport:
|
|
checks: tuple[DiagnosticCheck, ...]
|
|
|
|
@property
|
|
def is_healthy(self) -> bool:
|
|
return all(check.passed for check in self.checks)
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"is_healthy": self.is_healthy,
|
|
"checks": [asdict(check) for check in self.checks],
|
|
}
|
|
|
|
|
|
def run_doctor(settings: Settings) -> DoctorReport:
|
|
checks = [
|
|
DiagnosticCheck(
|
|
"python",
|
|
sys.version_info >= (3, 12),
|
|
f"Python {sys.version_info.major}.{sys.version_info.minor}",
|
|
),
|
|
DiagnosticCheck(
|
|
"cli",
|
|
importlib.util.find_spec("gyxx_flow.__main__") is not None,
|
|
"gyxx_flow module entry point is importable",
|
|
),
|
|
DiagnosticCheck(
|
|
"project_root",
|
|
settings.project_root.is_dir(),
|
|
"project root exists" if settings.project_root.is_dir() else "project root is missing",
|
|
),
|
|
]
|
|
try:
|
|
catalog = WorkflowCatalog.load(settings.project_root / "config")
|
|
catalog_ok = len(catalog.scheduled_workflows()) == 23
|
|
catalog_message = f"catalog has {len(catalog.scheduled_workflows())} scheduled workflows"
|
|
except Exception:
|
|
catalog_ok = False
|
|
catalog_message = "catalog cannot be validated"
|
|
checks.append(DiagnosticCheck("catalog", catalog_ok, catalog_message))
|
|
|
|
data_root_ok = settings.data_root.is_absolute() and settings.data_root != Path(
|
|
settings.data_root.anchor
|
|
)
|
|
checks.append(
|
|
DiagnosticCheck(
|
|
"data_root",
|
|
data_root_ok,
|
|
"data root is an explicit non-root path" if data_root_ok else "unsafe data root",
|
|
)
|
|
)
|
|
writable, message = _probe_write_permission(settings.data_root)
|
|
checks.append(DiagnosticCheck("data_root_write", writable, message))
|
|
return DoctorReport(tuple(checks))
|
|
|
|
|
|
def _probe_write_permission(path: Path) -> tuple[bool, str]:
|
|
candidate = Path(path)
|
|
while not candidate.exists() and candidate.parent != candidate:
|
|
candidate = candidate.parent
|
|
if not candidate.is_dir():
|
|
return False, "no existing data-root parent directory"
|
|
try:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="wb", prefix=".gyxx-doctor-probe-", dir=candidate, delete=True
|
|
) as stream:
|
|
stream.write(b"probe")
|
|
stream.flush()
|
|
except OSError:
|
|
return False, "data-root parent is not writable"
|
|
return True, "data-root parent write probe passed"
|