add docstrings to public methods

This commit is contained in:
ars
2026-06-25 13:38:41 +03:00
parent de997a9518
commit a6d68b7a99
4 changed files with 60 additions and 8 deletions
+11
View File
@@ -18,22 +18,28 @@ DEFAULT_PRESET_DESCRIPTIONS = {
@dataclass(frozen=True)
class CommandAccessPolicy:
"""Whitelist controlling which command presets a client may run."""
allowed_commands: frozenset[str] = field(default_factory=frozenset)
full_access: bool = False
@classmethod
def allow(cls, command_names: Iterable[str]) -> "CommandAccessPolicy":
"""Allow exactly the named command presets."""
return cls(allowed_commands=frozenset(command_names))
@classmethod
def allow_all(cls) -> "CommandAccessPolicy":
"""Allow every command preset (use with care)."""
return cls(full_access=True)
@classmethod
def deny_all(cls) -> "CommandAccessPolicy":
"""Deny every command preset."""
return cls()
def allows(self, command_name: str) -> bool:
"""Return whether ``command_name`` is permitted by this policy."""
return self.full_access or command_name in self.allowed_commands
@@ -83,6 +89,11 @@ def execute_command(
access_policy: CommandAccessPolicy = DEFAULT_COMMAND_ACCESS_POLICY,
output_limit_bytes: int = DEFAULT_OUTPUT_LIMIT_BYTES,
) -> CommandResult:
"""Run a command preset safely and return its :class:`CommandResult`.
Only known, allowed presets are executed (never arbitrary shell); output is
size-limited and each run is bound by ``command.timeout_seconds``.
"""
if command.kind != CommandKind.EXEC:
return _reject(command, f"unsupported command kind: {command.kind}")
+7
View File
@@ -81,6 +81,7 @@ def find_client_config_path(
cwd: Path | None = None,
home: Path | None = None,
) -> Path | None:
"""Find the client config (nexus.yml/yaml) in cwd, then XDG, then ~/.config."""
cwd = cwd or Path.cwd()
home = home or Path.home()
candidates = [
@@ -110,6 +111,7 @@ def load_client_config(
*,
config_path: Path | str | None = None,
) -> ClientConfig:
"""Load and validate the client config, returning a :class:`ClientConfig`."""
path = Path(config_path) if config_path is not None else find_client_config_path(env=env)
if path is None:
raise ClientConfigError("client config file not found")
@@ -145,6 +147,7 @@ def build_heartbeat_request(
*,
last_command_result: CommandResult | None = None,
) -> HeartbeatRequest:
"""Build a heartbeat payload from config and an optional last command result."""
now = datetime.now(UTC)
return HeartbeatRequest(
client_id=config.client_id,
@@ -173,6 +176,7 @@ def list_available_commands(
presets: Mapping[str, PresetBuilder] = DEFAULT_PRESETS,
descriptions: Mapping[str, str] = DEFAULT_PRESET_DESCRIPTIONS,
) -> list[ClientCommandCapability]:
"""Return the capabilities the access policy allows, as advertised to the server."""
return [
ClientCommandCapability(
name=name,
@@ -189,6 +193,7 @@ def send_heartbeat(
*,
opener: HeartbeatOpener = urllib.request.urlopen,
) -> HeartbeatResponse:
"""POST a heartbeat and return the parsed response; raises :class:`HeartbeatError`."""
payload = heartbeat.model_dump_json().encode()
request = urllib.request.Request(
f"{config.server_url}{HEARTBEAT_PATH}",
@@ -227,6 +232,7 @@ def run_once(
] = send_heartbeat,
executor: Callable[..., CommandResult] = execute_command,
) -> CommandResult | None:
"""Send one heartbeat and execute the command the server returns, if any."""
response = heartbeat_sender(
config,
build_heartbeat_request(config, last_command_result=last_command_result),
@@ -242,6 +248,7 @@ def run_once(
def main(argv: list[str] | None = None) -> int:
"""Run the client once: load config, heartbeat, and drain any queued commands."""
_ = argv
try:
config = load_client_config()
+32
View File
@@ -6,10 +6,14 @@ from pydantic import BaseModel, ConfigDict, Field
class StrictBaseModel(BaseModel):
"""Base model that rejects unknown fields (``extra='forbid'``)."""
model_config = ConfigDict(extra="forbid")
class ClientPlatform(StrEnum):
"""Operating-system family reported by a client."""
LINUX = "linux"
DARWIN = "darwin"
WINDOWS = "windows"
@@ -17,10 +21,14 @@ class ClientPlatform(StrEnum):
class CommandKind(StrEnum):
"""Type of executor a command targets (only ``exec`` is defined so far)."""
EXEC = "exec"
class CommandResultStatus(StrEnum):
"""Terminal outcome a client reports for an executed command."""
SUCCEEDED = "succeeded"
FAILED = "failed"
TIMED_OUT = "timed_out"
@@ -28,6 +36,8 @@ class CommandResultStatus(StrEnum):
class CommandStatus(StrEnum):
"""Lifecycle state of a command on the server (pending → delivered → terminal)."""
PENDING = "pending"
DELIVERED = "delivered"
SUCCEEDED = "succeeded"
@@ -37,22 +47,30 @@ class CommandStatus(StrEnum):
class ClientInfo(StrictBaseModel):
"""Identifying details a client reports about itself in a heartbeat."""
hostname: str
platform: ClientPlatform
version: str
class ClientState(StrictBaseModel):
"""Lightweight runtime state a client reports (local time, uptime)."""
local_time: datetime
uptime_seconds: int | None = Field(default=None, ge=0)
class ClientCommandCapability(StrictBaseModel):
"""A command preset a client advertises as runnable (name and description)."""
name: str
description: str
class CommandResult(StrictBaseModel):
"""Outcome of one executed command, sent by the client to the server."""
command_id: str
status: CommandResultStatus
started_at: datetime | None = None
@@ -63,6 +81,8 @@ class CommandResult(StrictBaseModel):
class HeartbeatRequest(StrictBaseModel):
"""Payload a client POSTs each poll: identity, state, capabilities, last result."""
client_id: str
observed_at: datetime
client: ClientInfo
@@ -72,6 +92,8 @@ class HeartbeatRequest(StrictBaseModel):
class Command(StrictBaseModel):
"""A command the server hands to a client for execution."""
id: str
kind: CommandKind
name: str
@@ -80,6 +102,8 @@ class Command(StrictBaseModel):
class HeartbeatResponse(StrictBaseModel):
"""Server reply to a heartbeat: poll interval and an optional command to run."""
status: Literal["ok"] = "ok"
server_time: datetime
next_poll_after_seconds: int = Field(ge=0)
@@ -87,6 +111,8 @@ class HeartbeatResponse(StrictBaseModel):
class ClientRecord(StrictBaseModel):
"""Server-side stored view of a client and its last heartbeat."""
id: str
hostname: str
platform: ClientPlatform
@@ -99,6 +125,8 @@ class ClientRecord(StrictBaseModel):
class CommandRecord(StrictBaseModel):
"""Server-side stored command with delivery and lifecycle bookkeeping."""
id: str
client_id: str
kind: CommandKind
@@ -114,6 +142,8 @@ class CommandRecord(StrictBaseModel):
class CommandResultRecord(StrictBaseModel):
"""Server-side stored result reported by a client for a command."""
command_id: str
client_id: str
status: CommandResultStatus
@@ -126,6 +156,8 @@ class CommandResultRecord(StrictBaseModel):
class AuditLogRecord(StrictBaseModel):
"""Audit-trail entry recording who did what to which subject."""
id: str
actor: str
action: str
+10 -8
View File
@@ -15,29 +15,31 @@ from nexus_sync.common import (
class Store(Protocol):
"""Storage interface for clients, commands, and command results."""
def list_clients(self) -> list[ClientRecord]:
pass
"""Return all known clients."""
def get_client(self, client_id: str) -> ClientRecord | None:
pass
"""Return the client with ``client_id``, or ``None`` if unknown."""
def upsert_client(self, heartbeat: HeartbeatRequest, now: datetime) -> ClientRecord:
pass
"""Create or update a client from a heartbeat and return the stored record."""
def enqueue_command(self, command: Command, client_id: str, now: datetime) -> CommandRecord:
pass
"""Queue a pending command for a client and return the stored record."""
def record_command_result(self, heartbeat: HeartbeatRequest, now: datetime) -> None:
pass
"""Persist the command result carried by a heartbeat, if one is present."""
def take_next_command(self, client_id: str, now: datetime) -> Command | None:
pass
"""Pop the oldest pending command for a client, marking it delivered."""
def get_command(self, command_id: str) -> CommandRecord | None:
pass
"""Return the command with ``command_id``, or ``None`` if unknown."""
def get_command_result(self, command_id: str) -> CommandResultRecord | None:
pass
"""Return the latest result for ``command_id``, or ``None`` if none reported."""
@dataclass