merge: preserve Jupyter MIME output in Streamlit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
[browser]
|
||||||
|
gatherUsageStats = false
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# IPython Shell Execution
|
||||||
|
|
||||||
|
This project exposes persistent Python environments through web and model-facing adapters. The glossary keeps the long-lived execution environment distinct from each individual code execution.
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
**Shell**:
|
||||||
|
A persistent, isolated Python environment that retains its namespace and execution state across calls.
|
||||||
|
_Avoid_: Session, request, call
|
||||||
|
|
||||||
|
**Call**:
|
||||||
|
One request to execute code in one shell. A call has its own identity and lifecycle, can emit ordered events, can pause for input, and eventually produces a result or error.
|
||||||
|
_Avoid_: Shell, session, task
|
||||||
|
|
||||||
|
**Call ID**:
|
||||||
|
The application-facing identity of one call, used to associate its events, input requests, and final result. Any execution ID used internally by a transport is not part of this identity.
|
||||||
|
_Avoid_: Execution count, kernel ID
|
||||||
|
|
||||||
|
**Call Event**:
|
||||||
|
An ordered observable part of a call, such as standard output, standard error, display data, an input request, or an execution error.
|
||||||
|
_Avoid_: Log line, transport message
|
||||||
|
|
||||||
|
**App**:
|
||||||
|
The owner of the available shells and calls. It resolves shell selectors such as `new`, `last`, or a named shell and routes each call to its target shell.
|
||||||
|
_Avoid_: Server, session
|
||||||
|
|
||||||
|
**Shell Name**:
|
||||||
|
A user-facing selector for a shell. `new` requests a fresh shell, `last` selects the most recently selected shell, and any other name identifies a reusable named shell.
|
||||||
|
_Avoid_: Shell ID, kernel name
|
||||||
+11
-1
@@ -8,11 +8,14 @@ requires-python = ">=3.14"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.141.1",
|
"fastapi>=0.141.1",
|
||||||
"fastmcp>=3.4.7",
|
"fastmcp>=3.4.7",
|
||||||
|
"ipykernel>=7.3.0",
|
||||||
"ipython>=9.16.1",
|
"ipython>=9.16.1",
|
||||||
|
"jupyter-client>=8.10.0",
|
||||||
"langchain>=1.3.18",
|
"langchain>=1.3.18",
|
||||||
"langchain-openai>=1.6.0",
|
"langchain-openai>=1.6.0",
|
||||||
"matplotlib>=3.11.1",
|
"matplotlib>=3.11.1",
|
||||||
"matplotlib-inline>=0.1.7",
|
"matplotlib-inline>=0.1.7",
|
||||||
|
"pydantic>=2.13.5",
|
||||||
"uvicorn>=0.52.4",
|
"uvicorn>=0.52.4",
|
||||||
"wonderwords>=2.2.0",
|
"wonderwords>=2.2.0",
|
||||||
]
|
]
|
||||||
@@ -20,8 +23,10 @@ dependencies = [
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
ipython-demo = "ipython_shell.app:main"
|
ipython-demo = "ipython_shell.app:main"
|
||||||
ipython-shell = "ipython_shell.app:main"
|
ipython-shell = "ipython_shell.app:main"
|
||||||
ipython-webapp = "ipython_webapp.app:main"
|
ipython-webapp = "ipython_webapp:main"
|
||||||
|
ipython-jupyter-webapp = "ipython_webapp.jupyter.app:main"
|
||||||
ipython-mcp = "ipython_mcp.app:main"
|
ipython-mcp = "ipython_mcp.app:main"
|
||||||
|
ipython-st-demo = "st_demo.app:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.12.6,<0.13.0"]
|
requires = ["uv_build>=0.12.6,<0.13.0"]
|
||||||
@@ -29,3 +34,8 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[tool.uv.build-backend]
|
[tool.uv.build-backend]
|
||||||
module-name = "ipython_shell"
|
module-name = "ipython_shell"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"streamlit>=1.37",
|
||||||
|
]
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ from .events import (
|
|||||||
event_from_execution_result,
|
event_from_execution_result,
|
||||||
event_from_history_output,
|
event_from_history_output,
|
||||||
)
|
)
|
||||||
|
from .jupyter.app import JupyterApp
|
||||||
|
from .jupyter.shell import JupyterShell
|
||||||
|
from .jupyter.messages import JupyterMessage, KernelMessage, ParsedJupyterMessage
|
||||||
|
from .jupyter.transport import InputRequest, JupyterTransport
|
||||||
from .models import (
|
from .models import (
|
||||||
AppInfo,
|
AppInfo,
|
||||||
CallError,
|
CallError,
|
||||||
@@ -19,6 +23,7 @@ from .models import (
|
|||||||
ShellInfo,
|
ShellInfo,
|
||||||
ShellStatus,
|
ShellStatus,
|
||||||
)
|
)
|
||||||
|
from .registry import ShellRegistry
|
||||||
from .shell import (
|
from .shell import (
|
||||||
Shell,
|
Shell,
|
||||||
carefully_setup_shell,
|
carefully_setup_shell,
|
||||||
@@ -26,7 +31,7 @@ from .shell import (
|
|||||||
run_cell_and_collect,
|
run_cell_and_collect,
|
||||||
setup_shell,
|
setup_shell,
|
||||||
)
|
)
|
||||||
from .utils import generate_good_names
|
from .utils import generate_call_id, generate_good_names, generate_omfg_names
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"App",
|
"App",
|
||||||
@@ -38,15 +43,25 @@ __all__ = [
|
|||||||
"CallResponse",
|
"CallResponse",
|
||||||
"CallResult",
|
"CallResult",
|
||||||
"CallResultResponse",
|
"CallResultResponse",
|
||||||
|
"InputRequest",
|
||||||
"JSONValue",
|
"JSONValue",
|
||||||
|
"JupyterApp",
|
||||||
|
"JupyterShell",
|
||||||
|
"JupyterTransport",
|
||||||
|
"JupyterMessage",
|
||||||
|
"KernelMessage",
|
||||||
|
"ParsedJupyterMessage",
|
||||||
"Shell",
|
"Shell",
|
||||||
"ShellInfo",
|
"ShellInfo",
|
||||||
|
"ShellRegistry",
|
||||||
"ShellStatus",
|
"ShellStatus",
|
||||||
"carefully_setup_shell",
|
"carefully_setup_shell",
|
||||||
"error_from_execution_result",
|
"error_from_execution_result",
|
||||||
"event_from_execution_result",
|
"event_from_execution_result",
|
||||||
"event_from_history_output",
|
"event_from_history_output",
|
||||||
|
"generate_call_id",
|
||||||
"generate_good_names",
|
"generate_good_names",
|
||||||
|
"generate_omfg_names",
|
||||||
"isolate_output_history",
|
"isolate_output_history",
|
||||||
"run_cell_and_collect",
|
"run_cell_and_collect",
|
||||||
"setup_shell",
|
"setup_shell",
|
||||||
|
|||||||
+27
-33
@@ -7,9 +7,10 @@ from time import monotonic
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from ipython_shell.utils import generate_good_names
|
from ipython_shell.utils import generate_call_id
|
||||||
|
|
||||||
from .models import AppInfo, CallOptions, CallResponse, ShellInfo
|
from .models import AppInfo, CallOptions, CallResponse, ShellInfo
|
||||||
|
from .registry import ShellRegistry
|
||||||
from .shell import Shell
|
from .shell import Shell
|
||||||
|
|
||||||
|
|
||||||
@@ -21,52 +22,45 @@ class App:
|
|||||||
self.created_at = datetime.now(UTC).isoformat()
|
self.created_at = datetime.now(UTC).isoformat()
|
||||||
self._started_at = monotonic()
|
self._started_at = monotonic()
|
||||||
self._total_calls = 0
|
self._total_calls = 0
|
||||||
self.shells: dict[str, Shell] = {}
|
self._shell_registry = ShellRegistry(Shell)
|
||||||
self.last_shell: str | None = None
|
self._issued_call_ids: set[str] = set()
|
||||||
|
|
||||||
def _new_shell(self) -> tuple[str, Shell]:
|
@property
|
||||||
name = generate_good_names()
|
def shells(self) -> dict[str, Shell]:
|
||||||
while name in self.shells:
|
"""Expose the managed shells for compatibility with the old API."""
|
||||||
name = generate_good_names()
|
return self._shell_registry.shells
|
||||||
shell = Shell(name)
|
|
||||||
self.shells[name] = shell
|
@property
|
||||||
self.last_shell = name
|
def last_shell(self) -> str | None:
|
||||||
return name, shell
|
"""Return the name of the most recently selected shell."""
|
||||||
|
return self._shell_registry.last_shell
|
||||||
|
|
||||||
def _select_shell(self, shell_name: str) -> tuple[str, Shell]:
|
def _select_shell(self, shell_name: str) -> tuple[str, Shell]:
|
||||||
if shell_name == "new":
|
return self._shell_registry.select(shell_name)
|
||||||
return self._new_shell()
|
|
||||||
|
|
||||||
if shell_name == "last":
|
|
||||||
if self.last_shell is None:
|
|
||||||
return self._new_shell()
|
|
||||||
return self.last_shell, self.shells[self.last_shell]
|
|
||||||
|
|
||||||
if shell_name not in self.shells:
|
|
||||||
self.shells[shell_name] = Shell(shell_name)
|
|
||||||
self.last_shell = shell_name
|
|
||||||
return shell_name, self.shells[shell_name]
|
|
||||||
|
|
||||||
def list_shells(self) -> list[ShellInfo]:
|
def list_shells(self) -> list[ShellInfo]:
|
||||||
"""Return frontend-safe metadata for all known shells."""
|
"""Return frontend-safe metadata for all known shells."""
|
||||||
return [shell.describe() for shell in self.shells.values()]
|
return self._shell_registry.list_shells()
|
||||||
|
|
||||||
def create_shell(self) -> ShellInfo:
|
def create_shell(self) -> ShellInfo:
|
||||||
"""Create a fresh named shell and return its metadata."""
|
"""Create a fresh named shell and return its metadata."""
|
||||||
_, shell = self._new_shell()
|
return self._shell_registry.create_shell()
|
||||||
return shell.describe()
|
|
||||||
|
|
||||||
def get_shell(self, shell_name: str) -> ShellInfo:
|
def get_shell(self, shell_name: str) -> ShellInfo:
|
||||||
"""Return frontend-safe metadata for a single shell."""
|
"""Return frontend-safe metadata for a single shell."""
|
||||||
if shell_name not in self.shells:
|
return self._shell_registry.get_shell(shell_name)
|
||||||
raise KeyError(f"Shell {shell_name} does not exist")
|
|
||||||
return self.shells[shell_name].describe()
|
|
||||||
|
|
||||||
def get_last_shell(self) -> ShellInfo:
|
def get_last_shell(self) -> ShellInfo:
|
||||||
"""Return metadata for the most recently selected shell."""
|
"""Return metadata for the most recently selected shell."""
|
||||||
if self.last_shell is None:
|
return self._shell_registry.get_last_shell()
|
||||||
raise KeyError("No shell exists yet")
|
|
||||||
return self.get_shell(self.last_shell)
|
def _new_call_id(self) -> str:
|
||||||
|
"""Generate a readable call ID that is unique for this app."""
|
||||||
|
call_id = generate_call_id()
|
||||||
|
while call_id in self._issued_call_ids:
|
||||||
|
call_id = generate_call_id()
|
||||||
|
self._issued_call_ids.add(call_id)
|
||||||
|
return call_id
|
||||||
|
|
||||||
def info(self) -> AppInfo:
|
def info(self) -> AppInfo:
|
||||||
"""Return runtime metadata without initializing or inspecting shells."""
|
"""Return runtime metadata without initializing or inspecting shells."""
|
||||||
@@ -96,7 +90,7 @@ class App:
|
|||||||
if options is None:
|
if options is None:
|
||||||
options = CallOptions()
|
options = CallOptions()
|
||||||
selected_name, shell = self._select_shell(shell_name)
|
selected_name, shell = self._select_shell(shell_name)
|
||||||
call_id = uuid4().hex
|
call_id = self._new_call_id()
|
||||||
self._total_calls += 1
|
self._total_calls += 1
|
||||||
execution, events = shell.run_cell(code, call_id=call_id)
|
execution, events = shell.run_cell(code, call_id=call_id)
|
||||||
return CallResponse(
|
return CallResponse(
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from .app import JupyterApp
|
||||||
|
from .messages import (
|
||||||
|
DisplayDataMessage,
|
||||||
|
ErrorMessage,
|
||||||
|
ExecuteReplyMessage,
|
||||||
|
ExecuteResultMessage,
|
||||||
|
InputRequestMessage,
|
||||||
|
JupyterMessage,
|
||||||
|
KernelMessage,
|
||||||
|
KnownJupyterMessage,
|
||||||
|
ParsedJupyterMessage,
|
||||||
|
StatusMessage,
|
||||||
|
StreamMessage,
|
||||||
|
UnknownJupyterMessage,
|
||||||
|
parse_jupyter_message,
|
||||||
|
)
|
||||||
|
from .shell import JupyterShell
|
||||||
|
from .transport import InputRequest, JupyterTransport
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"InputRequest",
|
||||||
|
"DisplayDataMessage",
|
||||||
|
"ErrorMessage",
|
||||||
|
"ExecuteReplyMessage",
|
||||||
|
"ExecuteResultMessage",
|
||||||
|
"InputRequestMessage",
|
||||||
|
"JupyterApp",
|
||||||
|
"JupyterMessage",
|
||||||
|
"KernelMessage",
|
||||||
|
"JupyterShell",
|
||||||
|
"JupyterTransport",
|
||||||
|
"KnownJupyterMessage",
|
||||||
|
"ParsedJupyterMessage",
|
||||||
|
"StatusMessage",
|
||||||
|
"StreamMessage",
|
||||||
|
"UnknownJupyterMessage",
|
||||||
|
"parse_jupyter_message",
|
||||||
|
]
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from time import monotonic
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from ..models import AppInfo, ShellInfo
|
||||||
|
from ..registry import ShellRegistry
|
||||||
|
from ..utils import generate_call_id
|
||||||
|
from .messages import ParsedJupyterMessage
|
||||||
|
from .shell import JupyterShell
|
||||||
|
from .transport import InputRequest
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterApp:
|
||||||
|
"""Coordinate named persistent shells backed by Jupyter kernels."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.app_id = uuid4().hex
|
||||||
|
self.created_at = datetime.now(UTC).isoformat()
|
||||||
|
self._started_at = monotonic()
|
||||||
|
self._total_calls = 0
|
||||||
|
self._shell_registry = ShellRegistry(JupyterShell)
|
||||||
|
self._calls: dict[str, JupyterShell] = {}
|
||||||
|
self._issued_call_ids: set[str] = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shells(self) -> dict[str, JupyterShell]:
|
||||||
|
"""Expose the managed shells for compatibility with the sync app."""
|
||||||
|
return self._shell_registry.shells
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_shell(self) -> str | None:
|
||||||
|
"""Return the name of the most recently selected shell."""
|
||||||
|
return self._shell_registry.last_shell
|
||||||
|
|
||||||
|
def _select_shell(self, shell_name: str) -> tuple[str, JupyterShell]:
|
||||||
|
return self._shell_registry.select(shell_name)
|
||||||
|
|
||||||
|
def list_shells(self) -> list[ShellInfo]:
|
||||||
|
"""Return metadata for every known Jupyter shell."""
|
||||||
|
return self._shell_registry.list_shells()
|
||||||
|
|
||||||
|
def create_shell(self) -> ShellInfo:
|
||||||
|
"""Create a fresh Jupyter shell and return its metadata."""
|
||||||
|
return self._shell_registry.create_shell()
|
||||||
|
|
||||||
|
def get_shell(self, shell_name: str) -> ShellInfo:
|
||||||
|
"""Return frontend-safe metadata for one Jupyter shell."""
|
||||||
|
return self._shell_registry.get_shell(shell_name)
|
||||||
|
|
||||||
|
def get_last_shell(self) -> ShellInfo:
|
||||||
|
"""Return metadata for the most recently selected Jupyter shell."""
|
||||||
|
return self._shell_registry.get_last_shell()
|
||||||
|
|
||||||
|
def info(self) -> AppInfo:
|
||||||
|
"""Return runtime metadata without starting any kernels."""
|
||||||
|
return AppInfo(
|
||||||
|
app_id=self.app_id,
|
||||||
|
created_at=self.created_at,
|
||||||
|
uptime_seconds=monotonic() - self._started_at,
|
||||||
|
shell_count=len(self.shells),
|
||||||
|
last_shell=self.last_shell,
|
||||||
|
total_calls=self._total_calls,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _new_call_id(self) -> str:
|
||||||
|
"""Generate a readable call ID that is unique for this app."""
|
||||||
|
call_id = generate_call_id()
|
||||||
|
while call_id in self._issued_call_ids:
|
||||||
|
call_id = generate_call_id()
|
||||||
|
self._issued_call_ids.add(call_id)
|
||||||
|
return call_id
|
||||||
|
|
||||||
|
async def run_code_stream(
|
||||||
|
self,
|
||||||
|
code: str,
|
||||||
|
shell_name: str = "last",
|
||||||
|
) -> AsyncIterator[ParsedJupyterMessage | InputRequest]:
|
||||||
|
"""Execute code and yield its subprocess messages."""
|
||||||
|
_, shell = self._select_shell(shell_name)
|
||||||
|
call_id = self._new_call_id()
|
||||||
|
self._total_calls += 1
|
||||||
|
self._calls[call_id] = shell
|
||||||
|
try:
|
||||||
|
async for message in shell.run_cell_stream(code, call_id=call_id):
|
||||||
|
yield message
|
||||||
|
finally:
|
||||||
|
self._calls.pop(call_id, None)
|
||||||
|
|
||||||
|
async def reply_to_input(self, call_id: str, value: str) -> None:
|
||||||
|
"""Reply to an input request emitted by a running call."""
|
||||||
|
try:
|
||||||
|
shell = self._calls[call_id]
|
||||||
|
except KeyError as error:
|
||||||
|
raise KeyError(f"Call {call_id} does not exist") from error
|
||||||
|
await shell.reply_to_input(call_id, value)
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
"""Stop all kernel processes owned by this app."""
|
||||||
|
awaitables = [shell.shutdown() for shell in self.shells.values()]
|
||||||
|
for awaitable in awaitables:
|
||||||
|
await awaitable
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
from typing import Annotated, ClassVar, Final, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
||||||
|
|
||||||
|
|
||||||
|
type JupyterChannel = Literal["iopub", "shell", "stdin"]
|
||||||
|
type MessageStatus = Literal["ok", "error", "abort"]
|
||||||
|
|
||||||
|
STREAM: Final[Literal["stream"]] = "stream"
|
||||||
|
DISPLAY_DATA: Final[Literal["display_data"]] = "display_data"
|
||||||
|
UPDATE_DISPLAY_DATA: Final[Literal["update_display_data"]] = "update_display_data"
|
||||||
|
EXECUTE_RESULT: Final[Literal["execute_result"]] = "execute_result"
|
||||||
|
ERROR: Final[Literal["error"]] = "error"
|
||||||
|
STATUS: Final[Literal["status"]] = "status"
|
||||||
|
EXECUTE_INPUT: Final[Literal["execute_input"]] = "execute_input"
|
||||||
|
CLEAR_OUTPUT: Final[Literal["clear_output"]] = "clear_output"
|
||||||
|
EXECUTE_REPLY: Final[Literal["execute_reply"]] = "execute_reply"
|
||||||
|
INPUT_REQUEST: Final[Literal["input_request"]] = "input_request"
|
||||||
|
|
||||||
|
|
||||||
|
class MimeContent(BaseModel):
|
||||||
|
"""The shared payload for rich display messages."""
|
||||||
|
|
||||||
|
data: dict[str, object]
|
||||||
|
metadata: dict[str, object] = Field(default_factory=dict)
|
||||||
|
transient: dict[str, object] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ExecuteResultContent(MimeContent):
|
||||||
|
"""Rich display data plus the kernel execution counter."""
|
||||||
|
|
||||||
|
execution_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class StreamContent(BaseModel):
|
||||||
|
"""A stdout or stderr chunk emitted by the kernel."""
|
||||||
|
|
||||||
|
name: Literal["stdout", "stderr"]
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorContent(BaseModel):
|
||||||
|
"""An exception reported on the IOPub channel."""
|
||||||
|
|
||||||
|
ename: str
|
||||||
|
evalue: str
|
||||||
|
traceback: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ExecuteReplyContent(BaseModel):
|
||||||
|
"""The terminal shell-channel reply for an execution request."""
|
||||||
|
|
||||||
|
status: MessageStatus
|
||||||
|
execution_count: int | None = None
|
||||||
|
ename: str | None = None
|
||||||
|
evalue: str | None = None
|
||||||
|
traceback: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class StatusContent(BaseModel):
|
||||||
|
"""Kernel busy/idle state from an IOPub status message."""
|
||||||
|
|
||||||
|
execution_state: Literal["busy", "idle", "starting"]
|
||||||
|
|
||||||
|
|
||||||
|
class InputRequestContent(BaseModel):
|
||||||
|
"""The prompt and echo policy for a kernel input request."""
|
||||||
|
|
||||||
|
prompt: str
|
||||||
|
password: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExecuteInputContent(BaseModel):
|
||||||
|
"""The code and execution counter echoed by the kernel."""
|
||||||
|
|
||||||
|
code: str
|
||||||
|
execution_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class ClearOutputContent(BaseModel):
|
||||||
|
"""Whether a clear-output request should wait for new output."""
|
||||||
|
|
||||||
|
wait: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterMessage(BaseModel):
|
||||||
|
"""Common Jupyter envelope, also capable of holding unknown messages.
|
||||||
|
|
||||||
|
The protocol's message vocabulary is extensible, so ``msg_type`` stays a
|
||||||
|
string on this raw-compatible model. Known messages are narrowed into the
|
||||||
|
typed subclasses below by :func:`parse_jupyter_message`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
channel: JupyterChannel = "iopub"
|
||||||
|
msg_type: str
|
||||||
|
message_id: str = ""
|
||||||
|
parent_id: str | None = None
|
||||||
|
metadata: dict[str, object] = Field(default_factory=dict)
|
||||||
|
content: dict[str, object] = Field(default_factory=dict)
|
||||||
|
buffers: list[bytes] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class _TypedMessage(BaseModel):
|
||||||
|
"""Common fields for typed messages without narrowing mutable fields."""
|
||||||
|
|
||||||
|
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
message_id: str = ""
|
||||||
|
parent_id: str | None = None
|
||||||
|
metadata: dict[str, object] = Field(default_factory=dict)
|
||||||
|
buffers: list[bytes] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class StreamMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["stream"] = STREAM
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: StreamContent
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayDataMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["display_data"] = DISPLAY_DATA
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: MimeContent
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDisplayDataMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["update_display_data"] = UPDATE_DISPLAY_DATA
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: MimeContent
|
||||||
|
|
||||||
|
|
||||||
|
class ExecuteResultMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["execute_result"] = EXECUTE_RESULT
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: ExecuteResultContent
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["error"] = ERROR
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: ErrorContent
|
||||||
|
|
||||||
|
|
||||||
|
class StatusMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["status"] = STATUS
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: StatusContent
|
||||||
|
|
||||||
|
|
||||||
|
class ExecuteInputMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["execute_input"] = EXECUTE_INPUT
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: ExecuteInputContent
|
||||||
|
|
||||||
|
|
||||||
|
class ClearOutputMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["clear_output"] = CLEAR_OUTPUT
|
||||||
|
channel: Literal["iopub"] = "iopub"
|
||||||
|
content: ClearOutputContent
|
||||||
|
|
||||||
|
|
||||||
|
class ExecuteReplyMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["execute_reply"] = EXECUTE_REPLY
|
||||||
|
channel: Literal["shell"] = "shell"
|
||||||
|
content: ExecuteReplyContent
|
||||||
|
|
||||||
|
|
||||||
|
class InputRequestMessage(_TypedMessage):
|
||||||
|
msg_type: Literal["input_request"] = INPUT_REQUEST
|
||||||
|
channel: Literal["stdin"] = "stdin"
|
||||||
|
content: InputRequestContent
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownJupyterMessage(JupyterMessage):
|
||||||
|
"""An extension or future message preserved without lossy parsing."""
|
||||||
|
|
||||||
|
raw: dict[str, object] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
type KnownJupyterMessage = Annotated[
|
||||||
|
StreamMessage
|
||||||
|
| DisplayDataMessage
|
||||||
|
| UpdateDisplayDataMessage
|
||||||
|
| ExecuteResultMessage
|
||||||
|
| ErrorMessage
|
||||||
|
| StatusMessage
|
||||||
|
| ExecuteInputMessage
|
||||||
|
| ClearOutputMessage
|
||||||
|
| ExecuteReplyMessage
|
||||||
|
| InputRequestMessage,
|
||||||
|
Field(discriminator="msg_type"),
|
||||||
|
]
|
||||||
|
type ParsedJupyterMessage = KnownJupyterMessage | UnknownJupyterMessage
|
||||||
|
|
||||||
|
_KNOWN_MESSAGE_TYPES: Final[frozenset[str]] = frozenset(
|
||||||
|
{
|
||||||
|
STREAM,
|
||||||
|
DISPLAY_DATA,
|
||||||
|
UPDATE_DISPLAY_DATA,
|
||||||
|
EXECUTE_RESULT,
|
||||||
|
ERROR,
|
||||||
|
STATUS,
|
||||||
|
EXECUTE_INPUT,
|
||||||
|
CLEAR_OUTPUT,
|
||||||
|
EXECUTE_REPLY,
|
||||||
|
INPUT_REQUEST,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_KNOWN_MESSAGE_ADAPTER = TypeAdapter(KnownJupyterMessage)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_jupyter_message(
|
||||||
|
raw: dict[str, object],
|
||||||
|
*,
|
||||||
|
channel: JupyterChannel,
|
||||||
|
) -> ParsedJupyterMessage:
|
||||||
|
"""Parse a decoded Jupyter message without dropping extensions."""
|
||||||
|
parent_header = raw.get("parent_header")
|
||||||
|
parent_id = (
|
||||||
|
parent_header.get("msg_id")
|
||||||
|
if isinstance(parent_header, dict)
|
||||||
|
and isinstance(parent_header.get("msg_id"), str)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
raw_metadata = raw.get("metadata")
|
||||||
|
metadata = raw_metadata if isinstance(raw_metadata, dict) else {}
|
||||||
|
raw_content = raw.get("content")
|
||||||
|
content = raw_content if isinstance(raw_content, dict) else {}
|
||||||
|
raw_buffers = raw.get("buffers")
|
||||||
|
buffers = (
|
||||||
|
[
|
||||||
|
bytes(buffer)
|
||||||
|
for buffer in raw_buffers
|
||||||
|
if isinstance(buffer, (bytes, bytearray, memoryview))
|
||||||
|
]
|
||||||
|
if isinstance(raw_buffers, list)
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
raw_msg_type = raw.get("msg_type")
|
||||||
|
raw_message_id = raw.get("msg_id")
|
||||||
|
values: dict[str, object] = {
|
||||||
|
"channel": channel,
|
||||||
|
"msg_type": raw_msg_type if isinstance(raw_msg_type, str) else "",
|
||||||
|
"message_id": raw_message_id if isinstance(raw_message_id, str) else "",
|
||||||
|
"parent_id": parent_id,
|
||||||
|
"metadata": metadata,
|
||||||
|
"content": content,
|
||||||
|
"buffers": buffers,
|
||||||
|
}
|
||||||
|
|
||||||
|
if values["msg_type"] not in _KNOWN_MESSAGE_TYPES:
|
||||||
|
return UnknownJupyterMessage.model_validate({**values, "raw": raw})
|
||||||
|
return _KNOWN_MESSAGE_ADAPTER.validate_python(values)
|
||||||
|
|
||||||
|
|
||||||
|
# Existing callers use this name for the transport-level message. Keep it as
|
||||||
|
# an alias while the more expressive JupyterMessage name becomes canonical.
|
||||||
|
KernelMessage = JupyterMessage
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from ..models import ShellInfo, ShellStatus
|
||||||
|
from .messages import ExecuteReplyMessage, ParsedJupyterMessage
|
||||||
|
from .transport import InputRequest, JupyterTransport
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterShell:
|
||||||
|
"""One persistent, subprocess-backed IPython shell."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, transport: JupyterTransport | None = None) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.shell_id = uuid4().hex
|
||||||
|
self.created_at = datetime.now(UTC).isoformat()
|
||||||
|
self.last_used_at: str | None = None
|
||||||
|
self.status: ShellStatus = "ready"
|
||||||
|
self._last_execution_count = 0
|
||||||
|
self.transport = transport or JupyterTransport()
|
||||||
|
self._active_call_id: str | None = None
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""Start this shell's kernel if it is not already running."""
|
||||||
|
if self.transport.client is None:
|
||||||
|
await self.transport.start()
|
||||||
|
|
||||||
|
async def run_cell_stream(
|
||||||
|
self,
|
||||||
|
code: str,
|
||||||
|
*,
|
||||||
|
call_id: str,
|
||||||
|
) -> AsyncIterator[ParsedJupyterMessage | InputRequest]:
|
||||||
|
"""Yield kernel messages while one cell executes."""
|
||||||
|
await self.start()
|
||||||
|
self._active_call_id = call_id
|
||||||
|
self.status = "running"
|
||||||
|
try:
|
||||||
|
kernel_call_id = await self.transport.execute(code)
|
||||||
|
async for message in self.transport.messages_for(kernel_call_id):
|
||||||
|
if isinstance(message, InputRequest):
|
||||||
|
yield InputRequest(
|
||||||
|
call_id=call_id,
|
||||||
|
prompt=message.prompt,
|
||||||
|
password=message.password,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Keep the kernel's execution ID private to the transport.
|
||||||
|
public_message = message.model_copy(update={"parent_id": call_id})
|
||||||
|
if isinstance(public_message, ExecuteReplyMessage):
|
||||||
|
if public_message.content.execution_count is not None:
|
||||||
|
self._last_execution_count = (
|
||||||
|
public_message.content.execution_count
|
||||||
|
)
|
||||||
|
self.status = (
|
||||||
|
"error"
|
||||||
|
if public_message.content.status == "error"
|
||||||
|
else "ready"
|
||||||
|
)
|
||||||
|
self.last_used_at = datetime.now(UTC).isoformat()
|
||||||
|
yield public_message
|
||||||
|
finally:
|
||||||
|
if self._active_call_id == call_id:
|
||||||
|
self._active_call_id = None
|
||||||
|
if self.status == "running" and self._active_call_id is None:
|
||||||
|
self.status = "ready"
|
||||||
|
|
||||||
|
async def reply_to_input(self, call_id: str, value: str) -> None:
|
||||||
|
"""Send input to the cell currently waiting in this shell."""
|
||||||
|
if self._active_call_id != call_id:
|
||||||
|
raise RuntimeError(f"No active input request for call: {call_id}")
|
||||||
|
await self.transport.reply_to_input(value)
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
"""Stop this shell's kernel process."""
|
||||||
|
await self.transport.shutdown()
|
||||||
|
|
||||||
|
def describe(self) -> ShellInfo:
|
||||||
|
"""Return metadata without starting an unused kernel."""
|
||||||
|
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",
|
||||||
|
"input",
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from time import monotonic
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from jupyter_client.manager import AsyncKernelManager
|
||||||
|
|
||||||
|
from .messages import (
|
||||||
|
ExecuteReplyMessage,
|
||||||
|
InputRequestMessage,
|
||||||
|
JupyterChannel,
|
||||||
|
JupyterMessage,
|
||||||
|
KernelMessage,
|
||||||
|
ParsedJupyterMessage,
|
||||||
|
StatusMessage,
|
||||||
|
parse_jupyter_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InputRequest:
|
||||||
|
"""A request for user input emitted by a running kernel call."""
|
||||||
|
|
||||||
|
call_id: str
|
||||||
|
prompt: str
|
||||||
|
password: bool
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterTransport:
|
||||||
|
"""Own one persistent ipykernel subprocess and its message channels."""
|
||||||
|
|
||||||
|
_IOPUB_SETTLE_SECONDS = 0.25
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
matplotlib_backend: str | None = "module://matplotlib_inline.backend_inline",
|
||||||
|
) -> None:
|
||||||
|
self.manager = AsyncKernelManager()
|
||||||
|
self.matplotlib_backend = matplotlib_backend
|
||||||
|
self.client = None
|
||||||
|
self._active_call: str | None = None
|
||||||
|
self._call_lock = asyncio.Lock()
|
||||||
|
self._waiting_for_input: str | None = None
|
||||||
|
self._message_queue: (
|
||||||
|
asyncio.Queue[tuple[JupyterChannel, dict[str, Any] | BaseException]] | None
|
||||||
|
) = None
|
||||||
|
self._reader_tasks: set[asyncio.Task[None]] = set()
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""Start the kernel and its persistent channel readers."""
|
||||||
|
if self.client is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# The parent process may select a non-interactive backend (Streamlit
|
||||||
|
# sets ``MPLBACKEND=Agg``). That is useful for server-side rendering,
|
||||||
|
# but it prevents ipykernel from publishing rich display MIME. Give
|
||||||
|
# each Jupyter subprocess its own inline default without mutating the
|
||||||
|
# host process environment.
|
||||||
|
kernel_env = dict(os.environ)
|
||||||
|
if self.matplotlib_backend is not None:
|
||||||
|
kernel_env["MPLBACKEND"] = self.matplotlib_backend
|
||||||
|
await self.manager.start_kernel(env=kernel_env)
|
||||||
|
self.client = self.manager.client()
|
||||||
|
self.client.start_channels()
|
||||||
|
await self.client.wait_for_ready()
|
||||||
|
|
||||||
|
self._message_queue = asyncio.Queue()
|
||||||
|
self._reader_tasks = {
|
||||||
|
asyncio.create_task(
|
||||||
|
self._read_channel("iopub", self.client.get_iopub_msg),
|
||||||
|
name="jupyter-iopub-reader",
|
||||||
|
),
|
||||||
|
asyncio.create_task(
|
||||||
|
self._read_channel("shell", self.client.get_shell_msg),
|
||||||
|
name="jupyter-shell-reader",
|
||||||
|
),
|
||||||
|
asyncio.create_task(
|
||||||
|
self._read_channel("stdin", self.client.get_stdin_msg),
|
||||||
|
name="jupyter-stdin-reader",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _read_channel(
|
||||||
|
self,
|
||||||
|
channel: JupyterChannel,
|
||||||
|
get_message: Callable[..., Awaitable[dict[str, Any]]],
|
||||||
|
) -> None:
|
||||||
|
"""Read one ZMQ channel continuously into the transport queue."""
|
||||||
|
if self._message_queue is None:
|
||||||
|
raise RuntimeError("JupyterTransport has not been started")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
message = await get_message()
|
||||||
|
await self._message_queue.put((channel, message))
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except BaseException as error:
|
||||||
|
# Surface reader failures instead of leaving messages_for() stuck.
|
||||||
|
await self._message_queue.put((channel, error))
|
||||||
|
|
||||||
|
async def _stop_readers(self) -> None:
|
||||||
|
"""Cancel channel readers once, during transport shutdown."""
|
||||||
|
readers = self._reader_tasks
|
||||||
|
self._reader_tasks = set()
|
||||||
|
for reader in readers:
|
||||||
|
reader.cancel()
|
||||||
|
await asyncio.gather(*readers, return_exceptions=True)
|
||||||
|
self._message_queue = None
|
||||||
|
|
||||||
|
async def execute(self, code: str) -> str:
|
||||||
|
"""Submit one cell and return its Jupyter message ID."""
|
||||||
|
if self.client is None:
|
||||||
|
raise RuntimeError("JupyterTransport has not been started")
|
||||||
|
|
||||||
|
async with self._call_lock:
|
||||||
|
if self._active_call is not None:
|
||||||
|
raise RuntimeError("JupyterTransport already has an active call")
|
||||||
|
self._active_call = self.client.execute(code, allow_stdin=True)
|
||||||
|
return self._active_call
|
||||||
|
|
||||||
|
async def messages_for(
|
||||||
|
self, call_id: str
|
||||||
|
) -> AsyncIterator[ParsedJupyterMessage | InputRequest]:
|
||||||
|
"""Yield decoded output, input, and completion messages for one call."""
|
||||||
|
if self.client is None:
|
||||||
|
raise RuntimeError("JupyterTransport has not been started")
|
||||||
|
if self._active_call != call_id:
|
||||||
|
raise RuntimeError(f"Unknown or inactive call: {call_id}")
|
||||||
|
if self._message_queue is None:
|
||||||
|
raise RuntimeError("JupyterTransport message readers are not running")
|
||||||
|
|
||||||
|
reply: ExecuteReplyMessage | None = None
|
||||||
|
idle = False
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
channel, queued_message = await self._message_queue.get()
|
||||||
|
if isinstance(queued_message, BaseException):
|
||||||
|
raise queued_message
|
||||||
|
|
||||||
|
message = queued_message
|
||||||
|
parent_id = message.get("parent_header", {}).get("msg_id")
|
||||||
|
if parent_id != call_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
decoded = parse_jupyter_message(message, channel=channel)
|
||||||
|
|
||||||
|
if isinstance(decoded, InputRequestMessage):
|
||||||
|
self._waiting_for_input = call_id
|
||||||
|
yield InputRequest(
|
||||||
|
call_id=call_id,
|
||||||
|
prompt=decoded.content.prompt,
|
||||||
|
password=decoded.content.password,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(decoded, ExecuteReplyMessage):
|
||||||
|
# The shell reply and IOPub messages use different
|
||||||
|
# channels. Buffer the reply so it remains terminal
|
||||||
|
# even when both channels arrive close together.
|
||||||
|
reply = decoded
|
||||||
|
else:
|
||||||
|
yield decoded
|
||||||
|
if isinstance(decoded, StatusMessage) and (
|
||||||
|
decoded.content.execution_state == "idle"
|
||||||
|
):
|
||||||
|
idle = True
|
||||||
|
|
||||||
|
if reply is not None and idle:
|
||||||
|
# Shell and IOPub are independent ZMQ channels. The
|
||||||
|
# kernel can publish a final rich display after the shell
|
||||||
|
# reply and idle status have already reached this queue.
|
||||||
|
# Wait briefly for that cross-channel tail before ending
|
||||||
|
# the call, otherwise inline Matplotlib output can vanish.
|
||||||
|
settle_deadline = monotonic() + self._IOPUB_SETTLE_SECONDS
|
||||||
|
while True:
|
||||||
|
remaining = settle_deadline - monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
late_channel, late_message = await asyncio.wait_for(
|
||||||
|
self._message_queue.get(),
|
||||||
|
timeout=remaining,
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
break
|
||||||
|
if isinstance(late_message, BaseException):
|
||||||
|
raise late_message
|
||||||
|
late_parent_id = late_message.get("parent_header", {}).get(
|
||||||
|
"msg_id"
|
||||||
|
)
|
||||||
|
if late_parent_id != call_id:
|
||||||
|
continue
|
||||||
|
late_decoded = parse_jupyter_message(
|
||||||
|
late_message,
|
||||||
|
channel=late_channel,
|
||||||
|
)
|
||||||
|
if isinstance(late_decoded, InputRequestMessage):
|
||||||
|
self._waiting_for_input = call_id
|
||||||
|
yield InputRequest(
|
||||||
|
call_id=call_id,
|
||||||
|
prompt=late_decoded.content.prompt,
|
||||||
|
password=late_decoded.content.password,
|
||||||
|
)
|
||||||
|
elif isinstance(late_decoded, ExecuteReplyMessage):
|
||||||
|
reply = late_decoded
|
||||||
|
else:
|
||||||
|
yield late_decoded
|
||||||
|
yield reply
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
if self._waiting_for_input == call_id:
|
||||||
|
self._waiting_for_input = None
|
||||||
|
if self._active_call == call_id:
|
||||||
|
self._active_call = None
|
||||||
|
|
||||||
|
async def reply_to_input(self, value: str) -> None:
|
||||||
|
"""Reply to the active kernel input request without process stdin."""
|
||||||
|
if self.client is None:
|
||||||
|
raise RuntimeError("JupyterTransport has not been started")
|
||||||
|
if self._waiting_for_input is None:
|
||||||
|
raise RuntimeError("JupyterTransport is not waiting for input")
|
||||||
|
|
||||||
|
self.client.input(value)
|
||||||
|
self._waiting_for_input = None
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
"""Stop readers, channels, and the kernel process."""
|
||||||
|
await self._stop_readers()
|
||||||
|
if self.client is not None:
|
||||||
|
self.client.stop_channels()
|
||||||
|
self.client = None
|
||||||
|
await self.manager.shutdown_kernel(now=True)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from .models import ShellInfo
|
||||||
|
from .utils import generate_good_names
|
||||||
|
|
||||||
|
|
||||||
|
class DescribableShell(Protocol):
|
||||||
|
"""The small shell interface needed by the shared registry."""
|
||||||
|
|
||||||
|
def describe(self) -> ShellInfo: ...
|
||||||
|
|
||||||
|
|
||||||
|
class ShellRegistry[ShellT: DescribableShell]:
|
||||||
|
"""Own named shells and resolve ``new``, ``last``, and explicit names."""
|
||||||
|
|
||||||
|
def __init__(self, factory: Callable[[str], ShellT]) -> None:
|
||||||
|
self._factory = factory
|
||||||
|
self.shells: dict[str, ShellT] = {}
|
||||||
|
self.last_shell: str | None = None
|
||||||
|
|
||||||
|
def new_shell(self) -> tuple[str, ShellT]:
|
||||||
|
"""Create a fresh generated shell and select it."""
|
||||||
|
name = generate_good_names()
|
||||||
|
while name in self.shells:
|
||||||
|
name = generate_good_names()
|
||||||
|
shell = self._factory(name)
|
||||||
|
self.shells[name] = shell
|
||||||
|
self.last_shell = name
|
||||||
|
return name, shell
|
||||||
|
|
||||||
|
def select(self, shell_name: str) -> tuple[str, ShellT]:
|
||||||
|
"""Resolve a selector, creating explicit names when necessary."""
|
||||||
|
if shell_name == "new":
|
||||||
|
return self.new_shell()
|
||||||
|
if shell_name == "last":
|
||||||
|
if self.last_shell is None:
|
||||||
|
return self.new_shell()
|
||||||
|
return self.last_shell, self.shells[self.last_shell]
|
||||||
|
if shell_name not in self.shells:
|
||||||
|
self.shells[shell_name] = self._factory(shell_name)
|
||||||
|
self.last_shell = shell_name
|
||||||
|
return shell_name, self.shells[shell_name]
|
||||||
|
|
||||||
|
def list_shells(self) -> list[ShellInfo]:
|
||||||
|
"""Return metadata for every known shell."""
|
||||||
|
return [shell.describe() for shell in self.shells.values()]
|
||||||
|
|
||||||
|
def create_shell(self) -> ShellInfo:
|
||||||
|
"""Create a fresh generated shell and return its metadata."""
|
||||||
|
_, shell = self.new_shell()
|
||||||
|
return shell.describe()
|
||||||
|
|
||||||
|
def get_shell(self, shell_name: str) -> ShellInfo:
|
||||||
|
"""Return metadata for one existing shell."""
|
||||||
|
try:
|
||||||
|
shell = self.shells[shell_name]
|
||||||
|
except KeyError as error:
|
||||||
|
raise KeyError(f"Shell {shell_name} does not exist") from error
|
||||||
|
return shell.describe()
|
||||||
|
|
||||||
|
def get_last_shell(self) -> ShellInfo:
|
||||||
|
"""Return metadata for the most recently selected shell."""
|
||||||
|
if self.last_shell is None:
|
||||||
|
raise KeyError("No shell exists yet")
|
||||||
|
return self.get_shell(self.last_shell)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import secrets
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from wonderwords import RandomWord
|
from wonderwords import RandomWord
|
||||||
@@ -22,3 +25,46 @@ def generate_good_names() -> str:
|
|||||||
adjective = _WORD_GENERATOR.word(include_categories=["adjectives"])
|
adjective = _WORD_GENERATOR.word(include_categories=["adjectives"])
|
||||||
noun = _WORD_GENERATOR.word(include_categories=["nouns"])
|
noun = _WORD_GENERATOR.word(include_categories=["nouns"])
|
||||||
return f"{adjective}-{noun}-{uuid4().hex[:4]}"
|
return f"{adjective}-{noun}-{uuid4().hex[:4]}"
|
||||||
|
|
||||||
|
|
||||||
|
type WordCategory = Literal["adjectives", "nouns", "verbs"]
|
||||||
|
type WordCategory1 = Literal["adjective", "noun", "verb"]
|
||||||
|
type WordCategoryShort = Literal["adj", "n", "v"]
|
||||||
|
type NamePart = WordCategory | WordCategory1 | WordCategoryShort | int
|
||||||
|
|
||||||
|
_CATEGORY_NAMES: dict[
|
||||||
|
WordCategory | WordCategory1 | WordCategoryShort, WordCategory
|
||||||
|
] = {
|
||||||
|
"adjectives": "adjectives",
|
||||||
|
"adjective": "adjectives",
|
||||||
|
"adj": "adjectives",
|
||||||
|
"nouns": "nouns",
|
||||||
|
"noun": "nouns",
|
||||||
|
"n": "nouns",
|
||||||
|
"verbs": "verbs",
|
||||||
|
"verb": "verbs",
|
||||||
|
"v": "verbs",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_omfg_names(recipe: Sequence[NamePart]) -> str:
|
||||||
|
"""Generate a readable, collision-resistant shell name.
|
||||||
|
|
||||||
|
Example: generate_omfg_names(["adj", "n", "v", 4]) -> "happy-dog-run-1a2b"
|
||||||
|
"""
|
||||||
|
words: list[str] = []
|
||||||
|
for category in recipe:
|
||||||
|
if isinstance(category, int):
|
||||||
|
if category < 1:
|
||||||
|
raise ValueError("Hex suffix length must be positive")
|
||||||
|
words.append(secrets.token_hex((category + 1) // 2)[:category])
|
||||||
|
else:
|
||||||
|
words.append(
|
||||||
|
_WORD_GENERATOR.word(include_categories=[_CATEGORY_NAMES[category]])
|
||||||
|
)
|
||||||
|
return "-".join(words)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_call_id() -> str:
|
||||||
|
"""Generate a readable application-level identifier for one call."""
|
||||||
|
return generate_omfg_names(["adj", "n", "v", 4])
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
from .app import app as interactive_shell_app
|
||||||
|
from .jupyter import app as jupyter_app
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Run the Jupyter shell web app.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--jupyter",
|
||||||
|
action="store_true",
|
||||||
|
help="Run the Jupyter shell web app.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
if args.jupyter:
|
||||||
|
uvicorn.run(jupyter_app, host="::", port=8000, log_level="info")
|
||||||
|
else:
|
||||||
|
uvicorn.run(interactive_shell_app, host="::", port=8000, log_level="info")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""FastAPI adapter for the subprocess-backed Jupyter shell app."""
|
||||||
|
|
||||||
|
from .app import InputReply, app, serialize_event, shell_app
|
||||||
|
|
||||||
|
__all__ = ["InputReply", "app", "serialize_event", "shell_app"]
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Body, FastAPI, HTTPException, Path, Response
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ipython_shell.jupyter import InputRequest, JupyterApp, ParsedJupyterMessage
|
||||||
|
|
||||||
|
app = FastAPI(title="IPython Jupyter Web App", version="0.1.0")
|
||||||
|
shell_app = JupyterApp()
|
||||||
|
|
||||||
|
|
||||||
|
class InputReply(BaseModel):
|
||||||
|
"""Value supplied by a client answering a kernel input request."""
|
||||||
|
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_event(event: ParsedJupyterMessage | InputRequest) -> dict[str, object]:
|
||||||
|
"""Turn a shell event into JSON-safe data for a web client."""
|
||||||
|
if isinstance(event, InputRequest):
|
||||||
|
return {
|
||||||
|
"type": "input_request",
|
||||||
|
"call_id": event.call_id,
|
||||||
|
"prompt": event.prompt,
|
||||||
|
"password": event.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Some Jupyter messages keep binary payloads in message buffers. Base64
|
||||||
|
# makes those buffers safe to carry beside JSON MIME data in the stream.
|
||||||
|
content = event.content
|
||||||
|
if isinstance(content, BaseModel):
|
||||||
|
content = content.model_dump(mode="json")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "kernel_message",
|
||||||
|
"msg_type": event.msg_type,
|
||||||
|
"parent_id": event.parent_id,
|
||||||
|
"content": content,
|
||||||
|
"buffers": [
|
||||||
|
base64.b64encode(buffer).decode("ascii") for buffer in event.buffers
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _stream_events(code: str, shell_name: str) -> AsyncIterator[str]:
|
||||||
|
"""Encode one execution's events as newline-delimited JSON."""
|
||||||
|
async for event in shell_app.run_code_stream(code, shell_name):
|
||||||
|
yield json.dumps(serialize_event(event), ensure_ascii=False) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/shells/{shell_name}/run")
|
||||||
|
async def run_code(
|
||||||
|
shell_name: Annotated[
|
||||||
|
str,
|
||||||
|
Path(
|
||||||
|
...,
|
||||||
|
description="The Jupyter shell to run code in. Existing shell guess. 'new' creates a new shell, 'last' uses the most recent shell.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
code: Annotated[
|
||||||
|
str,
|
||||||
|
Body(..., description="The code to execute", media_type="text/plain"),
|
||||||
|
],
|
||||||
|
) -> StreamingResponse:
|
||||||
|
"""Run code and stream each Jupyter event as one JSON line."""
|
||||||
|
return StreamingResponse(
|
||||||
|
_stream_events(code, shell_name),
|
||||||
|
media_type="application/x-ndjson",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/calls/{call_id}/input", status_code=204)
|
||||||
|
async def reply_to_input(call_id: str, reply: InputReply) -> Response:
|
||||||
|
"""Answer an input request belonging to a currently running call."""
|
||||||
|
try:
|
||||||
|
await shell_app.reply_to_input(call_id, reply.value)
|
||||||
|
except KeyError as error:
|
||||||
|
raise HTTPException(status_code=404, detail=str(error)) from error
|
||||||
|
except RuntimeError as error:
|
||||||
|
raise HTTPException(status_code=409, detail=str(error)) from error
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run the Jupyter-backed HTTP API with Uvicorn."""
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="::", port=8001, log_level="info")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Streamlit demonstration client for the Jupyter-backed shell."""
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from concurrent.futures import Future
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from ipython_shell.jupyter import InputRequest, JupyterApp, ParsedJupyterMessage
|
||||||
|
|
||||||
|
from st_demo.ui import event_to_record, preferred_mime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _CallFinished:
|
||||||
|
"""Sentinel placed after a call's final Jupyter message."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _CallFailed:
|
||||||
|
"""Exception sentinel placed when the background call task fails."""
|
||||||
|
|
||||||
|
error: Exception
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _BackgroundCall:
|
||||||
|
events: queue.Queue[ParsedJupyterMessage | InputRequest | _CallFinished | _CallFailed]
|
||||||
|
task: Future[None] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ShellController:
|
||||||
|
"""Bridge Streamlit reruns to one persistent async Jupyter application."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._loop = asyncio.new_event_loop()
|
||||||
|
self._thread = threading.Thread(
|
||||||
|
target=self._run_loop,
|
||||||
|
name="st-demo-jupyter-loop",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._ready = threading.Event()
|
||||||
|
self._app: JupyterApp | None = None
|
||||||
|
self._calls: dict[str, _BackgroundCall] = {}
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._thread.start()
|
||||||
|
self._ready.wait(timeout=5)
|
||||||
|
if self._app is None:
|
||||||
|
raise RuntimeError("The Jupyter application did not start")
|
||||||
|
|
||||||
|
def _run_loop(self) -> None:
|
||||||
|
"""Own the asyncio loop so Streamlit's rerun loop stays synchronous."""
|
||||||
|
asyncio.set_event_loop(self._loop)
|
||||||
|
self._app = JupyterApp()
|
||||||
|
self._ready.set()
|
||||||
|
self._loop.run_forever()
|
||||||
|
self._loop.run_until_complete(self._app.shutdown())
|
||||||
|
self._loop.close()
|
||||||
|
|
||||||
|
def start_call(self, code: str, shell_name: str) -> str:
|
||||||
|
"""Start a call and return a local handle immediately."""
|
||||||
|
handle = uuid4().hex
|
||||||
|
background_call = _BackgroundCall(events=queue.Queue())
|
||||||
|
with self._lock:
|
||||||
|
self._calls[handle] = background_call
|
||||||
|
background_call.task = asyncio.run_coroutine_threadsafe(
|
||||||
|
self._consume_call(handle, code, shell_name),
|
||||||
|
self._loop,
|
||||||
|
)
|
||||||
|
return handle
|
||||||
|
|
||||||
|
async def _consume_call(self, handle: str, code: str, shell_name: str) -> None:
|
||||||
|
"""Consume the async app stream and publish events for Streamlit."""
|
||||||
|
if self._app is None:
|
||||||
|
raise RuntimeError("The Jupyter application did not start")
|
||||||
|
background_call = self._calls[handle]
|
||||||
|
try:
|
||||||
|
async for event in self._app.run_code_stream(code, shell_name):
|
||||||
|
background_call.events.put(event)
|
||||||
|
except Exception as error:
|
||||||
|
background_call.events.put(_CallFailed(error))
|
||||||
|
finally:
|
||||||
|
background_call.events.put(_CallFinished())
|
||||||
|
|
||||||
|
def poll(
|
||||||
|
self, handle: str
|
||||||
|
) -> tuple[list[ParsedJupyterMessage | InputRequest], bool, Exception | None]:
|
||||||
|
"""Drain events currently available for one call."""
|
||||||
|
try:
|
||||||
|
background_call = self._calls[handle]
|
||||||
|
except KeyError as error:
|
||||||
|
raise KeyError(f"Call {handle} does not exist") from error
|
||||||
|
|
||||||
|
events: list[ParsedJupyterMessage | InputRequest] = []
|
||||||
|
failed: Exception | None = None
|
||||||
|
finished = False
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
item = background_call.events.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
if isinstance(item, _CallFinished):
|
||||||
|
finished = True
|
||||||
|
elif isinstance(item, _CallFailed):
|
||||||
|
failed = item.error
|
||||||
|
else:
|
||||||
|
events.append(item)
|
||||||
|
return events, finished, failed
|
||||||
|
|
||||||
|
def reply_to_input(self, handle: str, call_id: str, value: str) -> None:
|
||||||
|
"""Answer an input request without blocking Streamlit's script."""
|
||||||
|
if handle not in self._calls:
|
||||||
|
raise KeyError(f"Call {handle} does not exist")
|
||||||
|
if self._app is None:
|
||||||
|
raise RuntimeError("The Jupyter application did not start")
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self._app.reply_to_input(call_id, value),
|
||||||
|
self._loop,
|
||||||
|
)
|
||||||
|
future.result(timeout=5)
|
||||||
|
|
||||||
|
def shell_names(self) -> list[str]:
|
||||||
|
"""Return known shell names without starting any unused kernels."""
|
||||||
|
if self._app is None:
|
||||||
|
return []
|
||||||
|
future = asyncio.run_coroutine_threadsafe(self._shell_names(), self._loop)
|
||||||
|
return future.result(timeout=5)
|
||||||
|
|
||||||
|
async def _shell_names(self) -> list[str]:
|
||||||
|
if self._app is None:
|
||||||
|
return []
|
||||||
|
return [shell.name for shell in self._app.list_shells()]
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Stop the background application when the Streamlit session ends."""
|
||||||
|
if self._loop.is_running():
|
||||||
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||||
|
self._thread.join(timeout=5)
|
||||||
|
|
||||||
|
|
||||||
|
def _controller() -> ShellController:
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
if "shell_controller" not in st.session_state:
|
||||||
|
st.session_state.shell_controller = ShellController()
|
||||||
|
return st.session_state.shell_controller
|
||||||
|
|
||||||
|
|
||||||
|
def _render_mime(data: dict[str, object]) -> None:
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
selected = preferred_mime(data)
|
||||||
|
if selected is None:
|
||||||
|
return
|
||||||
|
mime, value = selected
|
||||||
|
if mime == "text/html":
|
||||||
|
# This is explicitly a local demo: execute only code you trust.
|
||||||
|
st.html(str(value))
|
||||||
|
elif mime in {"image/png", "image/jpeg", "image/svg+xml"}:
|
||||||
|
if mime == "image/svg+xml":
|
||||||
|
st.html(str(value))
|
||||||
|
else:
|
||||||
|
st.image(base64.b64decode(str(value)))
|
||||||
|
elif mime == "application/pdf":
|
||||||
|
st.download_button(
|
||||||
|
"Download PDF",
|
||||||
|
base64.b64decode(str(value)),
|
||||||
|
file_name="output.pdf",
|
||||||
|
mime=mime,
|
||||||
|
)
|
||||||
|
elif mime == "application/javascript":
|
||||||
|
st.code(str(value), language="javascript")
|
||||||
|
elif mime == "text/latex":
|
||||||
|
st.latex(str(value))
|
||||||
|
else:
|
||||||
|
st.code(str(value), language="text")
|
||||||
|
|
||||||
|
|
||||||
|
def _render_record(record: dict[str, object]) -> None:
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
kind = record["kind"]
|
||||||
|
if kind == "stdout":
|
||||||
|
st.code(str(record.get("text", "")), language="text")
|
||||||
|
elif kind == "stderr":
|
||||||
|
st.error(str(record.get("text", "")))
|
||||||
|
elif kind in {"display_data", "execute_result"}:
|
||||||
|
data = record.get("data")
|
||||||
|
if isinstance(data, dict):
|
||||||
|
_render_mime(data)
|
||||||
|
elif kind == "error":
|
||||||
|
st.error(str(record.get("text", "")))
|
||||||
|
data = record.get("data")
|
||||||
|
if isinstance(data, dict):
|
||||||
|
traceback = data.get("traceback")
|
||||||
|
if isinstance(traceback, list):
|
||||||
|
st.code("".join(str(line) for line in traceback), language="text")
|
||||||
|
elif kind == "input_request":
|
||||||
|
st.info(str(record.get("prompt", "Input required")))
|
||||||
|
elif kind not in {"status", "execute_reply", "execute_input"}:
|
||||||
|
with st.expander(f"Jupyter message: {kind}"):
|
||||||
|
st.json(record)
|
||||||
|
|
||||||
|
|
||||||
|
def visible_records(
|
||||||
|
call_records: Mapping[str, list[dict[str, object]]],
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
"""Flatten stored calls in insertion order for persistent history."""
|
||||||
|
return [record for records in call_records.values() for record in records]
|
||||||
|
|
||||||
|
|
||||||
|
def _render_active_call(controller: ShellController) -> None:
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
handle = st.session_state.get("active_handle")
|
||||||
|
call_records = st.session_state.get("call_records", {})
|
||||||
|
if not isinstance(call_records, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
finished = False
|
||||||
|
failed: Exception | None = None
|
||||||
|
records: list[dict[str, object]] = []
|
||||||
|
if isinstance(handle, str):
|
||||||
|
events, finished, failed = controller.poll(handle)
|
||||||
|
records = call_records.setdefault(handle, [])
|
||||||
|
records.extend(event_to_record(event) for event in events)
|
||||||
|
|
||||||
|
for record in visible_records(call_records):
|
||||||
|
_render_record(record)
|
||||||
|
|
||||||
|
if not isinstance(handle, str):
|
||||||
|
return
|
||||||
|
|
||||||
|
input_record = next(
|
||||||
|
(record for record in reversed(records) if record["kind"] == "input_request"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if input_record is not None and not finished:
|
||||||
|
value = st.text_input(
|
||||||
|
str(input_record.get("prompt", "Input")),
|
||||||
|
type="password" if input_record.get("password") else "default",
|
||||||
|
key=f"input-value-{handle}",
|
||||||
|
)
|
||||||
|
if st.button("Send input", key=f"send-input-{handle}"):
|
||||||
|
call_id = str(input_record.get("call_id", ""))
|
||||||
|
controller.reply_to_input(handle, call_id, value)
|
||||||
|
st.rerun(scope="fragment")
|
||||||
|
elif failed is not None:
|
||||||
|
st.exception(failed)
|
||||||
|
elif not finished:
|
||||||
|
st.caption("Running…")
|
||||||
|
else:
|
||||||
|
st.success("Call complete")
|
||||||
|
st.session_state.active_handle = None
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
"""Render the Streamlit shell workbench."""
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
st.set_page_config(page_title="IPython Shell", layout="wide")
|
||||||
|
st.title("IPython Shell")
|
||||||
|
st.caption("A persistent Jupyter kernel with typed events and MIME output.")
|
||||||
|
|
||||||
|
controller = _controller()
|
||||||
|
names = controller.shell_names()
|
||||||
|
shell_options = ["last", "new", *names]
|
||||||
|
shell_name = st.sidebar.selectbox("Shell", list(dict.fromkeys(shell_options)))
|
||||||
|
st.sidebar.caption(f"Known shells: {len(names)}")
|
||||||
|
|
||||||
|
code = st.text_area(
|
||||||
|
"Code",
|
||||||
|
value="import matplotlib.pyplot as plt\nplt.plot([1, 2, 3], [4, 5, 6])",
|
||||||
|
height=180,
|
||||||
|
)
|
||||||
|
if st.button("Run", type="primary"):
|
||||||
|
handle = controller.start_call(code, shell_name)
|
||||||
|
st.session_state.active_handle = handle
|
||||||
|
st.session_state.call_records = getattr(
|
||||||
|
st.session_state,
|
||||||
|
"call_records",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
st.session_state.call_records[handle] = []
|
||||||
|
|
||||||
|
call_records = st.session_state.get("call_records", {})
|
||||||
|
active_call = st.session_state.get("active_handle")
|
||||||
|
if call_records:
|
||||||
|
st.divider()
|
||||||
|
st.subheader("Output")
|
||||||
|
if isinstance(active_call, str) and hasattr(st, "fragment"):
|
||||||
|
st.fragment(run_every="0.25s")(_render_active_call)(controller)
|
||||||
|
elif isinstance(active_call, str):
|
||||||
|
_render_active_call(controller)
|
||||||
|
else:
|
||||||
|
for record in visible_records(call_records):
|
||||||
|
_render_record(record)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Launch this module through Streamlit's CLI."""
|
||||||
|
from streamlit.web import cli as stcli
|
||||||
|
|
||||||
|
sys.argv = ["streamlit", "run", str(Path(__file__).resolve()), *sys.argv[1:]]
|
||||||
|
raise SystemExit(stcli.main())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
render()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ipython_shell.jupyter.messages import (
|
||||||
|
DisplayDataMessage,
|
||||||
|
ErrorMessage,
|
||||||
|
ExecuteResultMessage,
|
||||||
|
ParsedJupyterMessage,
|
||||||
|
StreamMessage,
|
||||||
|
UnknownJupyterMessage,
|
||||||
|
)
|
||||||
|
from ipython_shell.jupyter.transport import InputRequest
|
||||||
|
|
||||||
|
|
||||||
|
MIME_PRIORITY: Final[tuple[str, ...]] = (
|
||||||
|
"text/html",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"application/pdf",
|
||||||
|
"application/javascript",
|
||||||
|
"text/latex",
|
||||||
|
"text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def preferred_mime(data: Mapping[str, object]) -> tuple[str, object] | None:
|
||||||
|
"""Choose the richest representation the demo knows how to display."""
|
||||||
|
for mime in MIME_PRIORITY:
|
||||||
|
if mime in data:
|
||||||
|
return mime, data[mime]
|
||||||
|
return next(iter(data.items()), None)
|
||||||
|
|
||||||
|
|
||||||
|
def _content_record(content: object) -> dict[str, object]:
|
||||||
|
if isinstance(content, BaseModel):
|
||||||
|
return content.model_dump(mode="json")
|
||||||
|
if isinstance(content, dict):
|
||||||
|
return dict(content)
|
||||||
|
return {"value": content}
|
||||||
|
|
||||||
|
|
||||||
|
def event_to_record(
|
||||||
|
event: ParsedJupyterMessage | InputRequest,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""Convert one typed kernel event into a UI-friendly record."""
|
||||||
|
if isinstance(event, InputRequest):
|
||||||
|
return {
|
||||||
|
"kind": "input_request",
|
||||||
|
"call_id": event.call_id,
|
||||||
|
"prompt": event.prompt,
|
||||||
|
"password": event.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
call_id = event.parent_id
|
||||||
|
if isinstance(event, StreamMessage):
|
||||||
|
return {
|
||||||
|
"kind": "stdout" if event.content.name == "stdout" else "stderr",
|
||||||
|
"call_id": call_id,
|
||||||
|
"text": event.content.text,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(event, (DisplayDataMessage, ExecuteResultMessage)):
|
||||||
|
record: dict[str, object] = {
|
||||||
|
"kind": event.msg_type,
|
||||||
|
"call_id": call_id,
|
||||||
|
"data": dict(event.content.data),
|
||||||
|
"text": event.content.data.get("text/plain"),
|
||||||
|
"metadata": dict(event.content.metadata),
|
||||||
|
}
|
||||||
|
if event.content.transient is not None:
|
||||||
|
record["transient"] = dict(event.content.transient)
|
||||||
|
if isinstance(event, ExecuteResultMessage):
|
||||||
|
record["execution_count"] = event.content.execution_count
|
||||||
|
return record
|
||||||
|
|
||||||
|
if isinstance(event, ErrorMessage):
|
||||||
|
return {
|
||||||
|
"kind": "error",
|
||||||
|
"call_id": call_id,
|
||||||
|
"text": f"{event.content.ename}: {event.content.evalue}",
|
||||||
|
"data": _content_record(event.content),
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(event, UnknownJupyterMessage):
|
||||||
|
return {
|
||||||
|
"kind": event.msg_type,
|
||||||
|
"call_id": call_id,
|
||||||
|
"content": dict(event.content),
|
||||||
|
"metadata": dict(event.metadata),
|
||||||
|
"raw": dict(event.raw),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kind": event.msg_type,
|
||||||
|
"call_id": call_id,
|
||||||
|
"content": _content_record(event.content),
|
||||||
|
}
|
||||||
@@ -49,6 +49,14 @@ class AppTests(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_call_ids_are_readable(self):
|
||||||
|
app = App()
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
call = app.run_code("1 + 1", shell_name="new")
|
||||||
|
|
||||||
|
self.assertRegex(call.call_id, r"^[a-z]+-[a-z]+-[a-z]+-[0-9a-f]{4}$")
|
||||||
|
|
||||||
def test_last_reuses_shell_but_each_call_has_new_id(self):
|
def test_last_reuses_shell_but_each_call_has_new_id(self):
|
||||||
app = App()
|
app = App()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ipython_shell.jupyter.app import JupyterApp
|
||||||
|
from ipython_shell.jupyter.shell import JupyterShell
|
||||||
|
from ipython_shell.jupyter.messages import ExecuteResultMessage
|
||||||
|
from ipython_shell.jupyter.transport import InputRequest
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterShellTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_shell_keeps_namespace_between_cells(self):
|
||||||
|
shell = JupyterShell("persistent")
|
||||||
|
try:
|
||||||
|
first = [
|
||||||
|
message
|
||||||
|
async for message in shell.run_cell_stream(
|
||||||
|
"answer = 41; answer",
|
||||||
|
call_id="call-1",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
second = [
|
||||||
|
message
|
||||||
|
async for message in shell.run_cell_stream(
|
||||||
|
"answer + 1",
|
||||||
|
call_id="call-2",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
await shell.shutdown()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
next(
|
||||||
|
message.content.data["text/plain"]
|
||||||
|
for message in first
|
||||||
|
if isinstance(message, ExecuteResultMessage)
|
||||||
|
),
|
||||||
|
"41",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
next(
|
||||||
|
message.content.data["text/plain"]
|
||||||
|
for message in second
|
||||||
|
if isinstance(message, ExecuteResultMessage)
|
||||||
|
),
|
||||||
|
"42",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterAppTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_app_exposes_shared_shell_metadata_api(self):
|
||||||
|
app = JupyterApp()
|
||||||
|
try:
|
||||||
|
created = app.create_shell()
|
||||||
|
listed = app.list_shells()
|
||||||
|
last = app.get_last_shell()
|
||||||
|
selected = app.get_shell(created.name)
|
||||||
|
info = app.info()
|
||||||
|
finally:
|
||||||
|
await app.shutdown()
|
||||||
|
|
||||||
|
self.assertEqual(len(listed), 1)
|
||||||
|
self.assertEqual(created.shell_id, listed[0].shell_id)
|
||||||
|
self.assertEqual(created.shell_id, last.shell_id)
|
||||||
|
self.assertEqual(created.shell_id, selected.shell_id)
|
||||||
|
self.assertEqual(created.execution_count, 0)
|
||||||
|
self.assertEqual(info.shell_count, 1)
|
||||||
|
self.assertEqual(info.last_shell, created.name)
|
||||||
|
|
||||||
|
async def test_named_shells_have_isolated_namespaces(self):
|
||||||
|
app = JupyterApp()
|
||||||
|
try:
|
||||||
|
_alpha_setup = [
|
||||||
|
message
|
||||||
|
async for message in app.run_code_stream(
|
||||||
|
"alpha_only = 'alpha'",
|
||||||
|
shell_name="alpha",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
beta_view = [
|
||||||
|
message
|
||||||
|
async for message in app.run_code_stream(
|
||||||
|
"'alpha_only' in globals()",
|
||||||
|
shell_name="beta",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
alpha_view = [
|
||||||
|
message
|
||||||
|
async for message in app.run_code_stream(
|
||||||
|
"alpha_only",
|
||||||
|
shell_name="alpha",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
await app.shutdown()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
next(
|
||||||
|
message.content.data["text/plain"]
|
||||||
|
for message in beta_view
|
||||||
|
if isinstance(message, ExecuteResultMessage)
|
||||||
|
),
|
||||||
|
"False",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
next(
|
||||||
|
message.content.data["text/plain"]
|
||||||
|
for message in alpha_view
|
||||||
|
if isinstance(message, ExecuteResultMessage)
|
||||||
|
),
|
||||||
|
"'alpha'",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_app_routes_input_reply_to_the_call(self):
|
||||||
|
app = JupyterApp()
|
||||||
|
stream = app.run_code_stream(
|
||||||
|
"answer = input('name? '); answer",
|
||||||
|
shell_name="new",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
event = await asyncio.wait_for(anext(stream), timeout=5)
|
||||||
|
if isinstance(event, InputRequest):
|
||||||
|
input_request = event
|
||||||
|
break
|
||||||
|
|
||||||
|
self.assertIsInstance(input_request, InputRequest)
|
||||||
|
assert isinstance(input_request, InputRequest)
|
||||||
|
self.assertRegex(
|
||||||
|
input_request.call_id,
|
||||||
|
re.compile(r"^[a-z]+-[a-z]+-[a-z]+-[0-9a-f]{4}$"),
|
||||||
|
)
|
||||||
|
self.assertEqual(input_request.prompt, "name? ")
|
||||||
|
|
||||||
|
await app.reply_to_input(input_request.call_id, "Ada")
|
||||||
|
messages = [message async for message in stream]
|
||||||
|
finally:
|
||||||
|
await app.shutdown()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
next(
|
||||||
|
message.content.data["text/plain"]
|
||||||
|
for message in messages
|
||||||
|
if isinstance(message, ExecuteResultMessage)
|
||||||
|
),
|
||||||
|
"'Ada'",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ipython_shell.jupyter.transport import KernelMessage
|
||||||
|
from ipython_webapp.jupyter.app import (
|
||||||
|
InputReply,
|
||||||
|
app,
|
||||||
|
reply_to_input,
|
||||||
|
run_code,
|
||||||
|
serialize_event,
|
||||||
|
shell_app,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterWebAppTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
def test_serialize_event_encodes_binary_buffers(self):
|
||||||
|
event = KernelMessage(
|
||||||
|
msg_type="display_data",
|
||||||
|
parent_id="call-1",
|
||||||
|
content={"data": {"application/octet-stream": "present"}},
|
||||||
|
buffers=[b"\x00\xff"],
|
||||||
|
)
|
||||||
|
|
||||||
|
serialized = serialize_event(event)
|
||||||
|
|
||||||
|
self.assertEqual(serialized["buffers"], ["AP8="])
|
||||||
|
|
||||||
|
async def test_run_endpoint_streams_json_events_with_mime_data(self):
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://testserver",
|
||||||
|
) as client:
|
||||||
|
try:
|
||||||
|
response = await client.post(
|
||||||
|
"/shells/new/run",
|
||||||
|
content="{'answer': 42}",
|
||||||
|
headers={"content-type": "text/plain"},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
await shell_app.shutdown()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(
|
||||||
|
response.headers["content-type"].startswith("application/x-ndjson")
|
||||||
|
)
|
||||||
|
events = [json.loads(line) for line in response.text.splitlines()]
|
||||||
|
result = next(
|
||||||
|
event for event in events if event["msg_type"] == "execute_result"
|
||||||
|
)
|
||||||
|
self.assertEqual(result["content"]["data"]["text/plain"], "{'answer': 42}")
|
||||||
|
|
||||||
|
async def test_input_request_can_be_replied_to_while_run_streams(self):
|
||||||
|
try:
|
||||||
|
response = await run_code("new", "answer = input('name? '); answer")
|
||||||
|
self.assertEqual(response.media_type, "application/x-ndjson")
|
||||||
|
events = []
|
||||||
|
async for line in response.body_iterator:
|
||||||
|
event = json.loads(line)
|
||||||
|
events.append(event)
|
||||||
|
if event["type"] == "input_request":
|
||||||
|
reply = await reply_to_input(
|
||||||
|
event["call_id"],
|
||||||
|
InputReply(value="Ada"),
|
||||||
|
)
|
||||||
|
self.assertEqual(reply.status_code, 204)
|
||||||
|
finally:
|
||||||
|
await shell_app.shutdown()
|
||||||
|
|
||||||
|
result = next(
|
||||||
|
event for event in events if event.get("msg_type") == "execute_result"
|
||||||
|
)
|
||||||
|
self.assertEqual(result["content"]["data"]["text/plain"], "'Ada'")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ipython_shell.jupyter.messages import (
|
||||||
|
ExecuteResultMessage,
|
||||||
|
UnknownJupyterMessage,
|
||||||
|
)
|
||||||
|
from st_demo.app import visible_records
|
||||||
|
from st_demo.ui import event_to_record, preferred_mime
|
||||||
|
|
||||||
|
|
||||||
|
class UIDisplayTests(unittest.TestCase):
|
||||||
|
def test_completed_call_records_remain_visible_after_active_call_clears(self):
|
||||||
|
records = {
|
||||||
|
"finished-call": [{"kind": "execute_result", "text": "42"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(visible_records(records), records["finished-call"])
|
||||||
|
|
||||||
|
def test_app_module_loads_when_streamlit_executes_file_path(self):
|
||||||
|
app_path = Path(__file__).parents[1] / "src" / "st_demo" / "app.py"
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-c",
|
||||||
|
"import runpy, sys; runpy.run_path(sys.argv[1], run_name='st_demo_script')",
|
||||||
|
str(app_path),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
|
def test_preferred_mime_chooses_html_before_plain_text(self):
|
||||||
|
mime, value = preferred_mime(
|
||||||
|
{
|
||||||
|
"text/plain": "fallback",
|
||||||
|
"text/html": "<b>rich</b>",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual((mime, value), ("text/html", "<b>rich</b>"))
|
||||||
|
|
||||||
|
def test_execute_result_record_keeps_mime_and_repr(self):
|
||||||
|
event = ExecuteResultMessage(
|
||||||
|
message_id="message-1",
|
||||||
|
parent_id="call-1",
|
||||||
|
content={
|
||||||
|
"execution_count": 4,
|
||||||
|
"data": {
|
||||||
|
"text/plain": "Line2D(_line0)",
|
||||||
|
"image/png": "ZmFrZQ==",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
record = event_to_record(event)
|
||||||
|
|
||||||
|
self.assertEqual(record["kind"], "execute_result")
|
||||||
|
self.assertEqual(record["text"], "Line2D(_line0)")
|
||||||
|
self.assertEqual(record["data"]["image/png"], "ZmFrZQ==")
|
||||||
|
|
||||||
|
def test_unknown_message_record_keeps_raw_protocol_data(self):
|
||||||
|
event = UnknownJupyterMessage(
|
||||||
|
channel="iopub",
|
||||||
|
msg_type="vendor_extension",
|
||||||
|
message_id="message-2",
|
||||||
|
content={"value": 42},
|
||||||
|
raw={"msg_type": "vendor_extension", "content": {"value": 42}},
|
||||||
|
)
|
||||||
|
|
||||||
|
record = event_to_record(event)
|
||||||
|
|
||||||
|
self.assertEqual(record["kind"], "vendor_extension")
|
||||||
|
self.assertEqual(record["content"]["value"], 42)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from ipython_shell.jupyter.messages import (
|
||||||
|
DisplayDataMessage,
|
||||||
|
JupyterMessage,
|
||||||
|
UnknownJupyterMessage,
|
||||||
|
parse_jupyter_message,
|
||||||
|
)
|
||||||
|
from ipython_shell.jupyter.transport import InputRequest, JupyterTransport
|
||||||
|
|
||||||
|
|
||||||
|
class TransportTests(unittest.TestCase):
|
||||||
|
def test_kernel_message_keeps_content_and_buffers(self):
|
||||||
|
message = JupyterMessage(
|
||||||
|
msg_type="display_data",
|
||||||
|
parent_id="call-1",
|
||||||
|
content={"data": {"text/plain": "42"}},
|
||||||
|
buffers=[b"binary"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(message.parent_id, "call-1")
|
||||||
|
self.assertEqual(message.content["data"], {"text/plain": "42"})
|
||||||
|
self.assertEqual(message.buffers, [b"binary"])
|
||||||
|
|
||||||
|
def test_parser_types_known_message_content(self):
|
||||||
|
message = parse_jupyter_message(
|
||||||
|
{
|
||||||
|
"msg_id": "message-1",
|
||||||
|
"msg_type": "display_data",
|
||||||
|
"parent_header": {"msg_id": "call-1"},
|
||||||
|
"metadata": {},
|
||||||
|
"content": {
|
||||||
|
"data": {"text/plain": "42"},
|
||||||
|
"metadata": {},
|
||||||
|
},
|
||||||
|
"buffers": [b"binary"],
|
||||||
|
},
|
||||||
|
channel="iopub",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(message, DisplayDataMessage)
|
||||||
|
assert isinstance(message, DisplayDataMessage)
|
||||||
|
self.assertEqual(message.content.data["text/plain"], "42")
|
||||||
|
self.assertEqual(message.parent_id, "call-1")
|
||||||
|
self.assertEqual(message.buffers, [b"binary"])
|
||||||
|
|
||||||
|
def test_parser_preserves_unknown_message(self):
|
||||||
|
message = parse_jupyter_message(
|
||||||
|
{
|
||||||
|
"msg_id": "message-2",
|
||||||
|
"msg_type": "future_extension",
|
||||||
|
"parent_header": {"msg_id": "call-1"},
|
||||||
|
"metadata": {"vendor": "example"},
|
||||||
|
"content": {"answer": 42},
|
||||||
|
"buffers": [],
|
||||||
|
},
|
||||||
|
channel="iopub",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(message, UnknownJupyterMessage)
|
||||||
|
self.assertEqual(message.msg_type, "future_extension")
|
||||||
|
self.assertEqual(message.content["answer"], 42)
|
||||||
|
self.assertEqual(message.metadata["vendor"], "example")
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncTransportTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_transport_keeps_channel_readers_alive_during_a_call(self):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
await transport.start()
|
||||||
|
try:
|
||||||
|
readers = set(transport._reader_tasks)
|
||||||
|
call_id = await transport.execute("2 + 2")
|
||||||
|
messages = [message async for message in transport.messages_for(call_id)]
|
||||||
|
|
||||||
|
self.assertEqual(len(readers), 3)
|
||||||
|
self.assertEqual(transport._reader_tasks, readers)
|
||||||
|
self.assertTrue(all(not reader.done() for reader in readers))
|
||||||
|
finally:
|
||||||
|
await transport.shutdown()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
any(message.msg_type == "execute_result" for message in messages)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_transport_executes_and_returns_execute_reply(self):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
await transport.start()
|
||||||
|
try:
|
||||||
|
call_id = await transport.execute("2 + 2")
|
||||||
|
messages = [message async for message in transport.messages_for(call_id)]
|
||||||
|
finally:
|
||||||
|
await transport.shutdown()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
any(message.msg_type == "execute_result" for message in messages)
|
||||||
|
)
|
||||||
|
self.assertEqual(messages[-1].msg_type, "execute_reply")
|
||||||
|
self.assertEqual(messages[-1].content.status, "ok")
|
||||||
|
|
||||||
|
async def test_transport_preserves_mime_and_errors(self):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
await transport.start()
|
||||||
|
try:
|
||||||
|
display_id = await transport.execute(
|
||||||
|
"from IPython.display import display\n"
|
||||||
|
"display({'text/plain': 'hello'}, raw=True)"
|
||||||
|
)
|
||||||
|
display_messages = [
|
||||||
|
message async for message in transport.messages_for(display_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
error_id = await transport.execute("raise ValueError('boom')")
|
||||||
|
error_messages = [
|
||||||
|
message async for message in transport.messages_for(error_id)
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
await transport.shutdown()
|
||||||
|
|
||||||
|
display = next(
|
||||||
|
message
|
||||||
|
for message in display_messages
|
||||||
|
if message.msg_type == "display_data"
|
||||||
|
)
|
||||||
|
self.assertEqual(display.content.data["text/plain"], "hello")
|
||||||
|
|
||||||
|
error = next(
|
||||||
|
message for message in error_messages if message.msg_type == "error"
|
||||||
|
)
|
||||||
|
self.assertEqual(error.content.ename, "ValueError")
|
||||||
|
|
||||||
|
async def test_transport_waits_for_iopub_output_after_idle_and_reply(self):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
transport.client = object()
|
||||||
|
transport._active_call = "call-1"
|
||||||
|
transport._message_queue = asyncio.Queue()
|
||||||
|
transport._message_queue.put_nowait(
|
||||||
|
(
|
||||||
|
"shell",
|
||||||
|
{
|
||||||
|
"msg_id": "reply-1",
|
||||||
|
"msg_type": "execute_reply",
|
||||||
|
"parent_header": {"msg_id": "call-1"},
|
||||||
|
"metadata": {},
|
||||||
|
"content": {"status": "ok", "execution_count": 1},
|
||||||
|
"buffers": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
transport._message_queue.put_nowait(
|
||||||
|
(
|
||||||
|
"iopub",
|
||||||
|
{
|
||||||
|
"msg_id": "status-1",
|
||||||
|
"msg_type": "status",
|
||||||
|
"parent_header": {"msg_id": "call-1"},
|
||||||
|
"metadata": {},
|
||||||
|
"content": {"execution_state": "idle"},
|
||||||
|
"buffers": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def enqueue_late_display() -> None:
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
await transport._message_queue.put(
|
||||||
|
(
|
||||||
|
"iopub",
|
||||||
|
{
|
||||||
|
"msg_id": "display-1",
|
||||||
|
"msg_type": "display_data",
|
||||||
|
"parent_header": {"msg_id": "call-1"},
|
||||||
|
"metadata": {},
|
||||||
|
"content": {
|
||||||
|
"data": {"image/png": "ZmFrZQ=="},
|
||||||
|
"metadata": {},
|
||||||
|
},
|
||||||
|
"buffers": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(enqueue_late_display())
|
||||||
|
messages = [message async for message in transport.messages_for("call-1")]
|
||||||
|
|
||||||
|
self.assertTrue(any(message.msg_type == "display_data" for message in messages))
|
||||||
|
|
||||||
|
async def test_transport_preserves_matplotlib_mime_output(self):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
await transport.start()
|
||||||
|
try:
|
||||||
|
call_id = await transport.execute(
|
||||||
|
"import matplotlib.pyplot as plt\nplt.plot([1, 2, 3], [4, 5, 6])"
|
||||||
|
)
|
||||||
|
messages = [message async for message in transport.messages_for(call_id)]
|
||||||
|
finally:
|
||||||
|
await transport.shutdown()
|
||||||
|
|
||||||
|
display = next(
|
||||||
|
message for message in messages if message.msg_type == "display_data"
|
||||||
|
)
|
||||||
|
self.assertIn("image/png", display.content.data)
|
||||||
|
|
||||||
|
async def test_transport_keeps_mime_output_when_parent_forces_agg(self):
|
||||||
|
# Streamlit sets MPLBACKEND=Agg in its own process. The kernel must
|
||||||
|
# still use an inline backend so rich output survives the transport.
|
||||||
|
with patch.dict(os.environ, {"MPLBACKEND": "Agg"}):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
await transport.start()
|
||||||
|
try:
|
||||||
|
call_id = await transport.execute(
|
||||||
|
"import matplotlib.pyplot as plt\n"
|
||||||
|
"plt.plot([1, 2, 3], [4, 5, 6])"
|
||||||
|
)
|
||||||
|
messages = [
|
||||||
|
message
|
||||||
|
async for message in transport.messages_for(call_id)
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
await transport.shutdown()
|
||||||
|
|
||||||
|
display = next(
|
||||||
|
message for message in messages if message.msg_type == "display_data"
|
||||||
|
)
|
||||||
|
self.assertIn("image/png", display.content.data)
|
||||||
|
|
||||||
|
async def test_transport_routes_input_reply_without_parent_stdin(self):
|
||||||
|
transport = JupyterTransport()
|
||||||
|
await transport.start()
|
||||||
|
try:
|
||||||
|
call_id = await transport.execute("answer = input('name? '); answer")
|
||||||
|
stream = transport.messages_for(call_id)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = await asyncio.wait_for(anext(stream), timeout=5)
|
||||||
|
if isinstance(event, InputRequest):
|
||||||
|
input_request = event
|
||||||
|
break
|
||||||
|
|
||||||
|
self.assertEqual(input_request.prompt, "name? ")
|
||||||
|
await transport.reply_to_input("Ada")
|
||||||
|
messages = [message async for message in stream]
|
||||||
|
finally:
|
||||||
|
await transport.shutdown()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
any(message.msg_type == "execute_result" for message in messages)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
|
resolution-markers = [
|
||||||
|
"sys_platform == 'win32'",
|
||||||
|
"sys_platform == 'emscripten'",
|
||||||
|
"sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofile"
|
name = "aiofile"
|
||||||
@@ -14,6 +19,22 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4e/79/6e45e778c4c3cab39e0937b007b720c15f76c50c6453d153282d0fcc3588/aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017", size = 22122, upload-time = "2026-08-04T22:59:25.838Z" },
|
{ url = "https://files.pythonhosted.org/packages/4e/79/6e45e778c4c3cab39e0937b007b720c15f76c50c6453d153282d0fcc3588/aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017", size = 22122, upload-time = "2026-08-04T22:59:25.838Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "altair"
|
||||||
|
version = "6.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "jsonschema" },
|
||||||
|
{ name = "narwhals" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/a1/5e6cc638a66da48cfc89a79c2f4810dfec00b63385f9b009ab1f069779bb/altair-6.2.2.tar.gz", hash = "sha256:a1ff9d9cfe81c75414641826312b9471780e19d39293ba0b012933f6b6cba0fe", size = 766606, upload-time = "2026-06-23T12:47:13.384Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/99/d6031f4f146298951c46b1bf1cc160c2a63f6e44b3c13a30054add100d5f/altair-6.2.2-py3-none-any.whl", hash = "sha256:94014f8ad8617c3cb163d1137359cd6db5ba134b9b46d93cfd8b609fd245a583", size = 797613, upload-time = "2026-06-23T12:47:11.451Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-doc"
|
name = "annotated-doc"
|
||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
@@ -44,6 +65,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "appnope"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/f7/a82489c2b6ebe32d3e2831895ae19c77861f0eadf3bb16034484d965dbb2/appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49", size = 5454, upload-time = "2026-08-20T21:36:13.748Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/c7/6b687cb0f83d2a51017d47953f2ee430ffad6fec2a8ebc964e0718a33eb1/appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e", size = 4158, upload-time = "2026-08-20T21:36:12.444Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asttokens"
|
name = "asttokens"
|
||||||
version = "3.0.2"
|
version = "3.0.2"
|
||||||
@@ -84,6 +114,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blinker"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "7.1.7"
|
version = "7.1.7"
|
||||||
@@ -292,6 +331,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "comm"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "contourpy"
|
name = "contourpy"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@@ -399,6 +447,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/00/d0/247938ef46dd81ab293efcac06ac2fee922db5bede9039d059912081db51/cyclopts-4.23.3-py3-none-any.whl", hash = "sha256:b3a65872942afb08f3ab5ca3d65b0b3ecfc872c9ccbea9d6a74ec11aa8a0215e", size = 237180, upload-time = "2026-08-26T18:16:34.128Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/d0/247938ef46dd81ab293efcac06ac2fee922db5bede9039d059912081db51/cyclopts-4.23.3-py3-none-any.whl", hash = "sha256:b3a65872942afb08f3ab5ca3d65b0b3ecfc872c9ccbea9d6a74ec11aa8a0215e", size = 237180, upload-time = "2026-08-26T18:16:34.128Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "debugpy"
|
||||||
|
version = "1.8.21"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "distro"
|
name = "distro"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -605,6 +666,28 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httptools"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
@@ -663,6 +746,30 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipykernel"
|
||||||
|
version = "7.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "appnope", marker = "sys_platform == 'darwin'" },
|
||||||
|
{ name = "comm" },
|
||||||
|
{ name = "debugpy" },
|
||||||
|
{ name = "ipython" },
|
||||||
|
{ name = "jupyter-client" },
|
||||||
|
{ name = "jupyter-core" },
|
||||||
|
{ name = "matplotlib-inline" },
|
||||||
|
{ name = "nest-asyncio2" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "psutil" },
|
||||||
|
{ name = "pyzmq" },
|
||||||
|
{ name = "tornado" },
|
||||||
|
{ name = "traitlets" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython"
|
name = "ipython"
|
||||||
version = "9.17.0"
|
version = "9.17.0"
|
||||||
@@ -691,28 +798,42 @@ source = { editable = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "fastmcp" },
|
{ name = "fastmcp" },
|
||||||
|
{ name = "ipykernel" },
|
||||||
{ name = "ipython" },
|
{ name = "ipython" },
|
||||||
|
{ name = "jupyter-client" },
|
||||||
{ name = "langchain" },
|
{ name = "langchain" },
|
||||||
{ name = "langchain-openai" },
|
{ name = "langchain-openai" },
|
||||||
{ name = "matplotlib" },
|
{ name = "matplotlib" },
|
||||||
{ name = "matplotlib-inline" },
|
{ name = "matplotlib-inline" },
|
||||||
|
{ name = "pydantic" },
|
||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
{ name = "wonderwords" },
|
{ name = "wonderwords" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "streamlit" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastapi", specifier = ">=0.141.1" },
|
{ name = "fastapi", specifier = ">=0.141.1" },
|
||||||
{ name = "fastmcp", specifier = ">=3.4.7" },
|
{ name = "fastmcp", specifier = ">=3.4.7" },
|
||||||
|
{ name = "ipykernel", specifier = ">=7.3.0" },
|
||||||
{ name = "ipython", specifier = ">=9.16.1" },
|
{ name = "ipython", specifier = ">=9.16.1" },
|
||||||
|
{ name = "jupyter-client", specifier = ">=8.10.0" },
|
||||||
{ name = "langchain", specifier = ">=1.3.18" },
|
{ name = "langchain", specifier = ">=1.3.18" },
|
||||||
{ name = "langchain-openai", specifier = ">=1.6.0" },
|
{ name = "langchain-openai", specifier = ">=1.6.0" },
|
||||||
{ name = "matplotlib", specifier = ">=3.11.1" },
|
{ name = "matplotlib", specifier = ">=3.11.1" },
|
||||||
{ name = "matplotlib-inline", specifier = ">=0.1.7" },
|
{ name = "matplotlib-inline", specifier = ">=0.1.7" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.13.5" },
|
||||||
{ name = "uvicorn", specifier = ">=0.52.4" },
|
{ name = "uvicorn", specifier = ">=0.52.4" },
|
||||||
{ name = "wonderwords", specifier = ">=2.2.0" },
|
{ name = "wonderwords", specifier = ">=2.2.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "streamlit", specifier = ">=1.37" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython-pygments-lexers"
|
name = "ipython-pygments-lexers"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -725,6 +846,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
|
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jaraco-classes"
|
name = "jaraco-classes"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@@ -779,6 +909,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiter"
|
name = "jiter"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -899,6 +1041,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jupyter-client"
|
||||||
|
version = "8.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jupyter-core" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pyzmq" },
|
||||||
|
{ name = "tornado" },
|
||||||
|
{ name = "traitlets" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184, upload-time = "2026-08-28T12:17:09.028Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jupyter-core"
|
||||||
|
version = "5.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "traitlets" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyring"
|
name = "keyring"
|
||||||
version = "25.7.0"
|
version = "25.7.0"
|
||||||
@@ -1148,6 +1320,36 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matplotlib"
|
name = "matplotlib"
|
||||||
version = "3.11.1"
|
version = "3.11.1"
|
||||||
@@ -1236,6 +1438,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "narwhals"
|
||||||
|
version = "2.25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/6248dada39781db1ab3ebf08943080df0796098515a87f6f8696d14ec744/narwhals-2.25.0.tar.gz", hash = "sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2", size = 677076, upload-time = "2026-08-20T18:10:15.454Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/dc/55481808fd70ef1567cf13540ffd4702af3f74b112e35427564b03f79c2d/narwhals-2.25.0-py3-none-any.whl", hash = "sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f", size = 467373, upload-time = "2026-08-20T18:10:13.834Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nest-asyncio2"
|
||||||
|
version = "1.7.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.5.2"
|
version = "2.5.2"
|
||||||
@@ -1388,6 +1608,35 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parso"
|
name = "parso"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -1489,6 +1738,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "7.36.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/e7/0553e21d25ca4d9f573135775348a372c3ec34a93a71d5f297c3bac38341/protobuf-7.36.0.tar.gz", hash = "sha256:e8e09cb0d794c6687926fa558a8a6e72aa10edb997d5ca61da0765f12a3e00ea", size = 510034, upload-time = "2026-08-20T16:34:01.071Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/ae/58e3ca96cb2e118cc546b677359b3c6659f79a140935c08dec94c7998585/protobuf-7.36.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:9103532dffd80c6fab7e50c65a31007680a06eb57537d437bb1b35812c138a37", size = 453256, upload-time = "2026-08-20T16:33:53.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/15/5162230af4912697f0fe406f6800f80760945babcff0e2c2fe6c84ef2d5d/protobuf-7.36.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:bf94a5917c71058262de683669bc0a797a7669d3de71f0b36d058e3194f47b44", size = 341436, upload-time = "2026-08-20T16:33:55.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/09/1670b2bfc9a45e807e520c3e9be36524db9ccc7dc05ea17af7681cabdc61/protobuf-7.36.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:3297e60abdff301e5f74393d87f6cc59dacab5f024a89548a6e8de1d26576b16", size = 354440, upload-time = "2026-08-20T16:33:56.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/f8/bd5804695ba400e423c33fd4d9f58c28d86633d5ba1945c36ff3967d98cb/protobuf-7.36.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:70f5ec8eb0da81a44360c0dc0beac99a0d78071d21956a7076bae8bd2051841b", size = 340439, upload-time = "2026-08-20T16:33:56.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/9f/acd02338235a3e7d03168c4303478347b7624fc8189ff4e7f0d2654bbe86/protobuf-7.36.0-cp310-abi3-win32.whl", hash = "sha256:7326fd717bdc419162a735938d89d4032332bcc3408804012b24ff3a37086071", size = 440216, upload-time = "2026-08-20T16:33:57.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/4e/12cb93270967a2affff5b3f720694700d4d87712a67afd05c8cb3f6fa52c/protobuf-7.36.0-cp310-abi3-win_amd64.whl", hash = "sha256:1781cc1de61249b750848029bca452c0a8b7e990080316b9bbc2518b2117b488", size = 453731, upload-time = "2026-08-20T16:33:58.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/c3/629999e78d46c1115c11886d51c6bd68c17ce4a944f1ea3e153a91316a33/protobuf-7.36.0-py3-none-any.whl", hash = "sha256:53374d53fc29a67f7dbbf0ade47d7526a0f0137bf0f9c90e48d8a60790ef748c", size = 177024, upload-time = "2026-08-20T16:34:00.053Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psutil"
|
name = "psutil"
|
||||||
version = "7.2.2"
|
version = "7.2.2"
|
||||||
@@ -1554,6 +1818,28 @@ memory = [
|
|||||||
{ name = "cachetools" },
|
{ name = "cachetools" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyarrow"
|
||||||
|
version = "25.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
@@ -1638,6 +1924,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydeck"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f4/c9/f71032fca47ecc09d30904d9a610234b07139f89eabc2f054b141edcc30f/pydeck-0.9.3.tar.gz", hash = "sha256:695775cbfe51f5fdffbd9735ba469987fdc5efc96bc40a0ee4808170509c78b2", size = 5900912, upload-time = "2026-07-02T23:27:08.704Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/34/3998411437aff304a9ed4fa37a6fe1ef3132bcd2b5eac59851b80c86123c/pydeck-0.9.3-py2.py3-none-any.whl", hash = "sha256:d8a47c11c81fb12d51b1feb42427ff4f0e13cb599e48931021b2cba98b6849a6", size = 11428091, upload-time = "2026-07-02T23:27:06.399Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
@@ -1757,6 +2056,52 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyzmq"
|
||||||
|
version = "27.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "implementation_name == 'pypy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074, upload-time = "2026-08-20T19:06:40.601Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450, upload-time = "2026-08-20T19:06:52.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633, upload-time = "2026-08-20T19:06:53.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006, upload-time = "2026-08-20T19:06:55.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452, upload-time = "2026-08-20T19:06:59.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520, upload-time = "2026-08-20T19:07:01.283Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289, upload-time = "2026-08-20T19:07:02.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103, upload-time = "2026-08-20T19:07:13.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215, upload-time = "2026-08-20T19:07:15.069Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252, upload-time = "2026-08-20T19:07:16.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295, upload-time = "2026-08-20T19:07:17.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492, upload-time = "2026-08-20T19:07:19.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992, upload-time = "2026-08-20T19:07:20.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073, upload-time = "2026-08-20T19:07:31.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701, upload-time = "2026-08-20T19:07:32.895Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985, upload-time = "2026-08-20T19:07:34.19Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "referencing"
|
name = "referencing"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
@@ -1999,6 +2344,38 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" },
|
{ url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "streamlit"
|
||||||
|
version = "1.62.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "altair" },
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "blinker" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "httptools" },
|
||||||
|
{ name = "itsdangerous" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "protobuf" },
|
||||||
|
{ name = "pyarrow" },
|
||||||
|
{ name = "pydeck" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "toml" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "uvicorn" },
|
||||||
|
{ name = "watchdog", marker = "sys_platform != 'darwin'" },
|
||||||
|
{ name = "websockets" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/94/928c44a8b7bcd602fc4a16025e9868bcdb88b92bcdb2e53dec188d034fc4/streamlit-1.62.0.tar.gz", hash = "sha256:9d2571da6e6799cbaf0f59548f5773926260a87a69807cf3e2f0f68f9f5e4d45", size = 9883501, upload-time = "2026-08-19T18:31:22.864Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/57/78864764c53885db8a378cc1c47329b4b6e095f7ebd89cd1ffcca4027c89/streamlit-1.62.0-py3-none-any.whl", hash = "sha256:294dbcfe0d6531b0d8593a095e6872dcc6ec4b731723fbb318a0f8102e69162e", size = 10490146, upload-time = "2026-08-19T18:31:19.957Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tenacity"
|
name = "tenacity"
|
||||||
version = "9.1.4"
|
version = "9.1.4"
|
||||||
@@ -2048,6 +2425,32 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tornado"
|
||||||
|
version = "6.5.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "traitlets"
|
name = "traitlets"
|
||||||
version = "5.16.1"
|
version = "5.16.1"
|
||||||
@@ -2087,6 +2490,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uncalled-for"
|
name = "uncalled-for"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -2155,6 +2567,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "watchdog"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchfiles"
|
name = "watchfiles"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user