expose shell metadata in call responses
This commit is contained in:
@@ -11,7 +11,6 @@ dependencies = [
|
|||||||
"ipython>=9.16.1",
|
"ipython>=9.16.1",
|
||||||
"matplotlib>=3.11.1",
|
"matplotlib>=3.11.1",
|
||||||
"matplotlib-inline>=0.1.7",
|
"matplotlib-inline>=0.1.7",
|
||||||
"sympy>=1.14.0",
|
|
||||||
"wonderwords>=2.2.0",
|
"wonderwords>=2.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,15 @@ from .events import (
|
|||||||
event_from_execution_result,
|
event_from_execution_result,
|
||||||
event_from_history_output,
|
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 (
|
from .shell import (
|
||||||
Shell,
|
Shell,
|
||||||
isolate_output_history,
|
isolate_output_history,
|
||||||
@@ -26,6 +34,8 @@ __all__ = [
|
|||||||
"Shell",
|
"Shell",
|
||||||
"event_from_history_output",
|
"event_from_history_output",
|
||||||
"generate_good_names",
|
"generate_good_names",
|
||||||
|
"ShellInfo",
|
||||||
|
"ShellStatus",
|
||||||
"isolate_output_history",
|
"isolate_output_history",
|
||||||
"run_cell_and_collect",
|
"run_cell_and_collect",
|
||||||
"setup_shell",
|
"setup_shell",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from wonderwords import RandomWord
|
from wonderwords import RandomWord
|
||||||
|
|
||||||
from .models import CallResponse
|
from .models import CallResponse, ShellInfo
|
||||||
from .shell import Shell
|
from .shell import Shell
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +50,10 @@ class App:
|
|||||||
self.last_shell = shell_name
|
self.last_shell = shell_name
|
||||||
return shell_name, self.shells[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(
|
def run_code(
|
||||||
self,
|
self,
|
||||||
code: str,
|
code: str,
|
||||||
@@ -68,6 +72,7 @@ class App:
|
|||||||
collapsed=collapsed,
|
collapsed=collapsed,
|
||||||
result=result,
|
result=result,
|
||||||
events=events,
|
events=events,
|
||||||
|
shell=shell.describe(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ CallEventKind = Literal[
|
|||||||
"error",
|
"error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ShellStatus = Literal["ready", "running", "error"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CallEvent:
|
class CallEvent:
|
||||||
@@ -55,3 +57,17 @@ class CallResponse:
|
|||||||
collapsed: bool
|
collapsed: bool
|
||||||
result: CallResult
|
result: CallResult
|
||||||
events: list[CallEvent]
|
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 collections import defaultdict
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from IPython.core.interactiveshell import InteractiveShell
|
from IPython.core.interactiveshell import InteractiveShell
|
||||||
from IPython.core.pylabtools import (
|
from IPython.core.pylabtools import (
|
||||||
@@ -14,7 +16,7 @@ from .events import (
|
|||||||
event_from_execution_result,
|
event_from_execution_result,
|
||||||
event_from_history_output,
|
event_from_history_output,
|
||||||
)
|
)
|
||||||
from .models import CallEvent, CallResult
|
from .models import CallEvent, CallResult, ShellInfo, ShellStatus
|
||||||
|
|
||||||
|
|
||||||
class Shell:
|
class Shell:
|
||||||
@@ -22,6 +24,11 @@ class Shell:
|
|||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
self.name = name
|
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
|
self._shell: InteractiveShell | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -45,7 +52,40 @@ class Shell:
|
|||||||
call_id: str,
|
call_id: str,
|
||||||
) -> tuple[CallResult, list[CallEvent]]:
|
) -> tuple[CallResult, list[CallEvent]]:
|
||||||
"""Execute one cell and return its result plus ordered output events."""
|
"""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:
|
def isolate_output_history(shell: InteractiveShell) -> None:
|
||||||
@@ -76,7 +116,7 @@ def run_cell_and_collect(
|
|||||||
execution = shell.run_cell(code, store_history=True)
|
execution = shell.run_cell(code, store_history=True)
|
||||||
records = tuple(
|
records = tuple(
|
||||||
history_manager.outputs.get(execution.execution_count, ())
|
history_manager.outputs.get(execution.execution_count, ())
|
||||||
)
|
) if execution.execution_count is not None else ()
|
||||||
events = [
|
events = [
|
||||||
event_from_history_output(
|
event_from_history_output(
|
||||||
record,
|
record,
|
||||||
|
|||||||
@@ -73,6 +73,20 @@ class AppTests(unittest.TestCase):
|
|||||||
self.assertNotEqual(first.call_id, second.call_id)
|
self.assertNotEqual(first.call_id, second.call_id)
|
||||||
self.assertEqual(second.result.result, 42)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user