add common models definations
This commit is contained in:
+6
-1
@@ -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 = [
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user