expose shell metadata in call responses
This commit is contained in:
@@ -11,7 +11,6 @@ dependencies = [
|
||||
"ipython>=9.16.1",
|
||||
"matplotlib>=3.11.1",
|
||||
"matplotlib-inline>=0.1.7",
|
||||
"sympy>=1.14.0",
|
||||
"wonderwords>=2.2.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@ from .events import (
|
||||
event_from_execution_result,
|
||||
event_from_history_output,
|
||||
)
|
||||
from .models import CallError, CallEvent, CallEventKind, CallResponse, CallResult
|
||||
from .models import (
|
||||
CallError,
|
||||
CallEvent,
|
||||
CallEventKind,
|
||||
CallResponse,
|
||||
CallResult,
|
||||
ShellInfo,
|
||||
ShellStatus,
|
||||
)
|
||||
from .shell import (
|
||||
Shell,
|
||||
isolate_output_history,
|
||||
@@ -26,6 +34,8 @@ __all__ = [
|
||||
"Shell",
|
||||
"event_from_history_output",
|
||||
"generate_good_names",
|
||||
"ShellInfo",
|
||||
"ShellStatus",
|
||||
"isolate_output_history",
|
||||
"run_cell_and_collect",
|
||||
"setup_shell",
|
||||
|
||||
@@ -6,7 +6,7 @@ from uuid import uuid4
|
||||
|
||||
from wonderwords import RandomWord
|
||||
|
||||
from .models import CallResponse
|
||||
from .models import CallResponse, ShellInfo
|
||||
from .shell import Shell
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ class App:
|
||||
self.last_shell = shell_name
|
||||
return shell_name, self.shells[shell_name]
|
||||
|
||||
def list_shells(self) -> list[ShellInfo]:
|
||||
"""Return frontend-safe metadata for all known shells."""
|
||||
return [shell.describe() for shell in self.shells.values()]
|
||||
|
||||
def run_code(
|
||||
self,
|
||||
code: str,
|
||||
@@ -68,6 +72,7 @@ class App:
|
||||
collapsed=collapsed,
|
||||
result=result,
|
||||
events=events,
|
||||
shell=shell.describe(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ CallEventKind = Literal[
|
||||
"error",
|
||||
]
|
||||
|
||||
ShellStatus = Literal["ready", "running", "error"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallEvent:
|
||||
@@ -55,3 +57,17 @@ class CallResponse:
|
||||
collapsed: bool
|
||||
result: CallResult
|
||||
events: list[CallEvent]
|
||||
shell: ShellInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShellInfo:
|
||||
"""Frontend-safe snapshot describing one persistent shell."""
|
||||
|
||||
shell_id: str
|
||||
name: str
|
||||
status: ShellStatus
|
||||
execution_count: int
|
||||
created_at: str
|
||||
last_used_at: str | None
|
||||
capabilities: tuple[str, ...]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from IPython.core.interactiveshell import InteractiveShell
|
||||
from IPython.core.pylabtools import (
|
||||
@@ -14,7 +16,7 @@ from .events import (
|
||||
event_from_execution_result,
|
||||
event_from_history_output,
|
||||
)
|
||||
from .models import CallEvent, CallResult
|
||||
from .models import CallEvent, CallResult, ShellInfo, ShellStatus
|
||||
|
||||
|
||||
class Shell:
|
||||
@@ -22,6 +24,11 @@ class Shell:
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.shell_id = uuid4().hex
|
||||
self.created_at = datetime.now(timezone.utc).isoformat()
|
||||
self.last_used_at: str | None = None
|
||||
self.status: ShellStatus = "ready"
|
||||
self._last_execution_count = 0
|
||||
self._shell: InteractiveShell | None = None
|
||||
|
||||
@property
|
||||
@@ -45,7 +52,40 @@ class Shell:
|
||||
call_id: str,
|
||||
) -> tuple[CallResult, list[CallEvent]]:
|
||||
"""Execute one cell and return its result plus ordered output events."""
|
||||
return run_cell_and_collect(self.init_shell(), code, call_id=call_id)
|
||||
self.status = "running"
|
||||
try:
|
||||
call_result, events = run_cell_and_collect(
|
||||
self.init_shell(),
|
||||
code,
|
||||
call_id=call_id,
|
||||
)
|
||||
except Exception:
|
||||
self.status = "error"
|
||||
raise
|
||||
else:
|
||||
self.status = "ready"
|
||||
self.last_used_at = datetime.now(timezone.utc).isoformat()
|
||||
self._last_execution_count = call_result.execution_count or 0
|
||||
return call_result, events
|
||||
|
||||
def describe(self) -> ShellInfo:
|
||||
"""Return metadata without initializing an unused shell."""
|
||||
return ShellInfo(
|
||||
shell_id=self.shell_id,
|
||||
name=self.name,
|
||||
status=self.status,
|
||||
execution_count=self._last_execution_count,
|
||||
created_at=self.created_at,
|
||||
last_used_at=self.last_used_at,
|
||||
capabilities=(
|
||||
"stdout",
|
||||
"stderr",
|
||||
"display_data",
|
||||
"execute_result",
|
||||
"error",
|
||||
"matplotlib-inline",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def isolate_output_history(shell: InteractiveShell) -> None:
|
||||
@@ -76,7 +116,7 @@ def run_cell_and_collect(
|
||||
execution = shell.run_cell(code, store_history=True)
|
||||
records = tuple(
|
||||
history_manager.outputs.get(execution.execution_count, ())
|
||||
)
|
||||
) if execution.execution_count is not None else ()
|
||||
events = [
|
||||
event_from_history_output(
|
||||
record,
|
||||
|
||||
@@ -73,6 +73,20 @@ class AppTests(unittest.TestCase):
|
||||
self.assertNotEqual(first.call_id, second.call_id)
|
||||
self.assertEqual(second.result.result, 42)
|
||||
|
||||
def test_response_and_shell_list_expose_shell_info(self):
|
||||
app = App()
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
call = app.run_code("1 + 1", shell_name="new")
|
||||
|
||||
shells = app.list_shells()
|
||||
|
||||
self.assertEqual(len(shells), 1)
|
||||
self.assertEqual(call.shell.shell_id, shells[0].shell_id)
|
||||
self.assertEqual(call.shell.name, call.shell_name)
|
||||
self.assertEqual(call.shell.execution_count, call.result.execution_count)
|
||||
self.assertIn("execute_result", call.shell.capabilities)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user