add common models definations

This commit is contained in:
ars
2026-05-24 17:18:44 +03:00
parent f864653356
commit a1b2e522ab
4 changed files with 254 additions and 1 deletions
+6 -1
View File
@@ -9,7 +9,12 @@ description = "A utility that allows you to manage your computers from a central
readme = "README.md" readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [] dependencies = [
"fastapi>=0.115.0",
"pydantic>=2.10.0",
"sqlalchemy>=2.0.0",
"uvicorn[standard]>=0.34.0",
]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
+33
View File
@@ -0,0 +1,33 @@
from nexus_sync.common.models import (
AuditLogRecord,
ClientInfo,
ClientPlatform,
ClientRecord,
ClientState,
Command,
CommandKind,
CommandRecord,
CommandResult,
CommandResultRecord,
CommandResultStatus,
CommandStatus,
HeartbeatRequest,
HeartbeatResponse,
)
__all__ = [
"AuditLogRecord",
"ClientInfo",
"ClientPlatform",
"ClientRecord",
"ClientState",
"Command",
"CommandKind",
"CommandRecord",
"CommandResult",
"CommandResultRecord",
"CommandResultStatus",
"CommandStatus",
"HeartbeatRequest",
"HeartbeatResponse",
]
+128
View File
@@ -0,0 +1,128 @@
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
class StrictBaseModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class ClientPlatform(StrEnum):
LINUX = "linux"
DARWIN = "darwin"
WINDOWS = "windows"
UNKNOWN = "unknown"
class CommandKind(StrEnum):
EXEC = "exec"
class CommandResultStatus(StrEnum):
SUCCEEDED = "succeeded"
FAILED = "failed"
TIMED_OUT = "timed_out"
REJECTED = "rejected"
class CommandStatus(StrEnum):
PENDING = "pending"
DELIVERED = "delivered"
SUCCEEDED = "succeeded"
FAILED = "failed"
TIMED_OUT = "timed_out"
REJECTED = "rejected"
class ClientInfo(StrictBaseModel):
hostname: str
platform: ClientPlatform
version: str
class ClientState(StrictBaseModel):
local_time: datetime
uptime_seconds: int | None = Field(default=None, ge=0)
class CommandResult(StrictBaseModel):
command_id: str
status: CommandResultStatus
started_at: datetime | None = None
finished_at: datetime | None = None
return_code: int | None = None
stdout: str = ""
stderr: str = ""
class HeartbeatRequest(StrictBaseModel):
client_id: str
observed_at: datetime
client: ClientInfo
state: ClientState
last_command_result: CommandResult | None = None
class Command(StrictBaseModel):
id: str
kind: CommandKind
name: str
args: dict[str, Any] = Field(default_factory=dict)
timeout_seconds: int = Field(gt=0)
class HeartbeatResponse(StrictBaseModel):
status: Literal["ok"] = "ok"
server_time: datetime
next_poll_after_seconds: int = Field(ge=0)
command: Command | None = None
class ClientRecord(StrictBaseModel):
id: str
hostname: str
platform: ClientPlatform
version: str
created_at: datetime
last_seen_at: datetime
is_active: bool = True
token_hash: str | None = None
class CommandRecord(StrictBaseModel):
id: str
client_id: str
kind: CommandKind
name: str
args: dict[str, Any] = Field(default_factory=dict)
status: CommandStatus = CommandStatus.PENDING
timeout_seconds: int = Field(gt=0)
attempts: int = Field(default=0, ge=0)
max_attempts: int = Field(default=1, ge=1)
created_at: datetime
delivered_at: datetime | None = None
finished_at: datetime | None = None
class CommandResultRecord(StrictBaseModel):
command_id: str
client_id: str
status: CommandResultStatus
started_at: datetime | None = None
finished_at: datetime | None = None
return_code: int | None = None
stdout: str = ""
stderr: str = ""
received_at: datetime
class AuditLogRecord(StrictBaseModel):
id: str
actor: str
action: str
subject_type: str
subject_id: str
created_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
+87
View File
@@ -0,0 +1,87 @@
from datetime import UTC, datetime
from pydantic import ValidationError
from nexus_sync.common import (
ClientInfo,
ClientPlatform,
ClientState,
Command,
CommandKind,
CommandResult,
CommandResultStatus,
HeartbeatRequest,
HeartbeatResponse,
)
def test_heartbeat_request_accepts_current_mvp_payload() -> None:
payload = HeartbeatRequest(
client_id="macbook-pro-01",
observed_at=datetime(2026, 5, 24, 13, 20, 30, tzinfo=UTC),
client=ClientInfo(
hostname="macbook-pro.local",
platform=ClientPlatform.DARWIN,
version="0.1.0",
),
state=ClientState(
local_time=datetime(2026, 5, 24, 13, 20, 30, tzinfo=UTC),
uptime_seconds=1200,
),
last_command_result=None,
)
serialized = payload.model_dump(mode="json")
assert serialized["client_id"] == "macbook-pro-01"
assert serialized["client"]["platform"] == "darwin"
assert serialized["last_command_result"] is None
def test_heartbeat_response_accepts_one_structured_command() -> None:
response = HeartbeatResponse(
server_time=datetime(2026, 5, 24, 13, 20, 30, tzinfo=UTC),
next_poll_after_seconds=10,
command=Command(
id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
kind=CommandKind.EXEC,
name="network_interfaces",
args={},
timeout_seconds=30,
),
)
serialized = response.model_dump(mode="json")
assert serialized["status"] == "ok"
assert serialized["command"]["kind"] == "exec"
assert serialized["command"]["name"] == "network_interfaces"
def test_command_result_supports_rejected_commands() -> None:
result = CommandResult(
command_id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
status=CommandResultStatus.REJECTED,
stderr="unknown command preset",
)
serialized = result.model_dump(mode="json")
assert serialized["status"] == "rejected"
assert serialized["return_code"] is None
def test_models_reject_unknown_fields() -> None:
try:
Command(
id="cmd_01JY3H8V8W8P3FXDR3S2BM7M6B",
kind=CommandKind.EXEC,
name="network_interfaces",
args={},
timeout_seconds=30,
shell="ip a",
)
except ValidationError as error:
assert "shell" in str(error)
else:
raise AssertionError("Command model accepted an unknown field")