mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-28 07:13:04 +08:00
feat(python): expose sandbox status conditions in the SDK (#4436)
* feat(python): expose sandbox status conditions in the SDK Add SandboxConditionRef and surface status.conditions on SandboxStatusRef and SandboxRef. Callers need condition reasons and transition times to time terminal transitions and explain failures that happen before the main process starts, without reading the raw proto. Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com> * test(python): use real gateway condition values in conditions fixtures Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com> --------- Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from .sandbox import (
|
||||
Pager,
|
||||
Sandbox,
|
||||
SandboxClient,
|
||||
SandboxConditionRef,
|
||||
SandboxError,
|
||||
SandboxRef,
|
||||
SandboxSession,
|
||||
@@ -51,6 +52,7 @@ __all__ = [
|
||||
"ProviderCredentialValue",
|
||||
"Sandbox",
|
||||
"SandboxClient",
|
||||
"SandboxConditionRef",
|
||||
"SandboxError",
|
||||
"SandboxRef",
|
||||
"SandboxSession",
|
||||
|
||||
@@ -452,6 +452,15 @@ class ClientCredentialsAuth:
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SandboxConditionRef:
|
||||
type: str
|
||||
status: str
|
||||
reason: str = ""
|
||||
message: str = ""
|
||||
transition_time_ms: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SandboxStatusRef:
|
||||
phase: int
|
||||
@@ -460,6 +469,7 @@ class SandboxStatusRef:
|
||||
restart_count: int = 0
|
||||
next_restart_at_ms: int | None = None
|
||||
main_process_started_at_ms: int | None = None
|
||||
conditions: tuple[SandboxConditionRef, ...] = ()
|
||||
|
||||
|
||||
class ServiceAuthorizationMode(IntEnum):
|
||||
@@ -538,6 +548,10 @@ class SandboxRef:
|
||||
def current_policy_version(self) -> int:
|
||||
return self.status.current_policy_version
|
||||
|
||||
@property
|
||||
def conditions(self) -> tuple[SandboxConditionRef, ...]:
|
||||
return self.status.conditions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecChunk:
|
||||
@@ -1772,6 +1786,25 @@ def _serialize_python_callable(
|
||||
return base64.b64encode(payload).decode("ascii")
|
||||
|
||||
|
||||
def _sandbox_conditions(
|
||||
status: openshell_pb2.SandboxStatus | None,
|
||||
) -> tuple[SandboxConditionRef, ...]:
|
||||
if status is None:
|
||||
return ()
|
||||
return tuple(
|
||||
SandboxConditionRef(
|
||||
type=condition.type,
|
||||
status=condition.status,
|
||||
reason=condition.reason,
|
||||
message=condition.message,
|
||||
transition_time_ms=condition.transition_time.ToMilliseconds()
|
||||
if condition.HasField("transition_time")
|
||||
else None,
|
||||
)
|
||||
for condition in status.conditions
|
||||
)
|
||||
|
||||
|
||||
def _sandbox_ref(
|
||||
sandbox: openshell_pb2.Sandbox,
|
||||
service_urls: Mapping[str, str] | None = None,
|
||||
@@ -1802,6 +1835,7 @@ def _sandbox_ref(
|
||||
main_process_started_at_ms=status.main_process_started_time.ToMilliseconds()
|
||||
if status is not None and status.HasField("main_process_started_time")
|
||||
else None,
|
||||
conditions=_sandbox_conditions(status),
|
||||
),
|
||||
labels=sandbox.metadata.labels if sandbox.metadata else {},
|
||||
created_from_workload_template=provenance,
|
||||
|
||||
@@ -28,6 +28,7 @@ from openshell.sandbox import (
|
||||
Pager,
|
||||
Sandbox,
|
||||
SandboxClient,
|
||||
SandboxConditionRef,
|
||||
SandboxError,
|
||||
SandboxRef,
|
||||
SandboxStatusRef,
|
||||
@@ -2750,6 +2751,63 @@ def test_sandbox_ref_includes_main_process_result() -> None:
|
||||
assert status.main_process_started_at_ms == 1_699_999_000_000
|
||||
|
||||
|
||||
def test_sandbox_ref_includes_conditions() -> None:
|
||||
proto = _make_sandbox_proto("sandbox-1", "job-1")
|
||||
|
||||
ready = proto.status.conditions.add()
|
||||
ready.type = "Ready"
|
||||
ready.status = "True"
|
||||
ready.reason = "BackendReady"
|
||||
ready.message = "Container is running"
|
||||
ready.transition_time.FromMilliseconds(1_700_000_000_000)
|
||||
|
||||
failed = proto.status.conditions.add()
|
||||
failed.type = "Ready"
|
||||
failed.status = "Unknown"
|
||||
failed.reason = "InspectFailed"
|
||||
failed.message = "Container inspect failed: connection refused"
|
||||
|
||||
ref = _sandbox_ref(proto)
|
||||
|
||||
assert ref.status.conditions == (
|
||||
SandboxConditionRef(
|
||||
type="Ready",
|
||||
status="True",
|
||||
reason="BackendReady",
|
||||
message="Container is running",
|
||||
transition_time_ms=1_700_000_000_000,
|
||||
),
|
||||
SandboxConditionRef(
|
||||
type="Ready",
|
||||
status="Unknown",
|
||||
reason="InspectFailed",
|
||||
message="Container inspect failed: connection refused",
|
||||
transition_time_ms=None,
|
||||
),
|
||||
)
|
||||
# Surfaces the same conditions through SandboxRef directly.
|
||||
assert ref.conditions == ref.status.conditions
|
||||
|
||||
|
||||
def test_sandbox_ref_conditions_default_to_empty() -> None:
|
||||
ref = _sandbox_ref(_make_sandbox_proto("sandbox-1", "job-1"))
|
||||
|
||||
assert ref.status.conditions == ()
|
||||
assert ref.conditions == ()
|
||||
|
||||
|
||||
def test_direct_sandbox_ref_construction_defaults_conditions() -> None:
|
||||
ref = SandboxRef(
|
||||
id="sandbox-1",
|
||||
name="job-1",
|
||||
workspace="default",
|
||||
status=SandboxStatusRef(phase=2, current_policy_version=0),
|
||||
)
|
||||
|
||||
assert ref.status.conditions == ()
|
||||
assert ref.conditions == ()
|
||||
|
||||
|
||||
def test_returned_labels_are_immutable() -> None:
|
||||
proto = _make_sandbox_proto("sandbox-1", "job-1", {"aiq": "deep-research"})
|
||||
ref = _sandbox_ref(proto)
|
||||
|
||||
Reference in New Issue
Block a user