feat: preserve Jupyter MIME output in Streamlit

This commit is contained in:
lda
2026-08-31 00:52:34 +07:00 Verified
parent 6a8546cb0f
commit 0754cb2508
16 changed files with 1315 additions and 75 deletions
+2
View File
@@ -0,0 +1,2 @@
[browser]
gatherUsageStats = false
+7
View File
@@ -15,6 +15,7 @@ dependencies = [
"langchain-openai>=1.6.0",
"matplotlib>=3.11.1",
"matplotlib-inline>=0.1.7",
"pydantic>=2.13.5",
"uvicorn>=0.52.4",
"wonderwords>=2.2.0",
]
@@ -25,6 +26,7 @@ ipython-shell = "ipython_shell.app:main"
ipython-webapp = "ipython_webapp:main"
ipython-jupyter-webapp = "ipython_webapp.jupyter.app:main"
ipython-mcp = "ipython_mcp.app:main"
ipython-st-demo = "st_demo.app:main"
[build-system]
requires = ["uv_build>=0.12.6,<0.13.0"]
@@ -32,3 +34,8 @@ build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "ipython_shell"
[dependency-groups]
dev = [
"streamlit>=1.37",
]
+4 -1
View File
@@ -8,7 +8,8 @@ from .events import (
)
from .jupyter.app import JupyterApp
from .jupyter.shell import JupyterShell
from .jupyter.transport import InputRequest, JupyterTransport, KernelMessage
from .jupyter.messages import JupyterMessage, KernelMessage, ParsedJupyterMessage
from .jupyter.transport import InputRequest, JupyterTransport
from .models import (
AppInfo,
CallError,
@@ -47,7 +48,9 @@ __all__ = [
"JupyterApp",
"JupyterShell",
"JupyterTransport",
"JupyterMessage",
"KernelMessage",
"ParsedJupyterMessage",
"Shell",
"ShellInfo",
"ShellRegistry",
+29 -2
View File
@@ -1,11 +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, KernelMessage
from .transport import InputRequest, JupyterTransport
__all__ = [
"InputRequest",
"DisplayDataMessage",
"ErrorMessage",
"ExecuteReplyMessage",
"ExecuteResultMessage",
"InputRequestMessage",
"JupyterApp",
"JupyterMessage",
"KernelMessage",
"JupyterShell",
"JupyterTransport",
"KernelMessage",
"KnownJupyterMessage",
"ParsedJupyterMessage",
"StatusMessage",
"StreamMessage",
"UnknownJupyterMessage",
"parse_jupyter_message",
]
+3 -2
View File
@@ -6,8 +6,9 @@ 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, KernelMessage
from .transport import InputRequest
class JupyterApp:
@@ -74,7 +75,7 @@ class JupyterApp:
self,
code: str,
shell_name: str = "last",
) -> AsyncIterator[KernelMessage | InputRequest]:
) -> AsyncIterator[ParsedJupyterMessage | InputRequest]:
"""Execute code and yield its subprocess messages."""
_, shell = self._select_shell(shell_name)
call_id = self._new_call_id()
+260
View File
@@ -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
+14 -11
View File
@@ -1,10 +1,10 @@
from collections.abc import AsyncIterator
from dataclasses import replace
from datetime import UTC, datetime
from uuid import uuid4
from ..models import ShellInfo, ShellStatus
from .transport import InputRequest, JupyterTransport, KernelMessage
from .messages import ExecuteReplyMessage, ParsedJupyterMessage
from .transport import InputRequest, JupyterTransport
class JupyterShell:
@@ -30,7 +30,7 @@ class JupyterShell:
code: str,
*,
call_id: str,
) -> AsyncIterator[KernelMessage | InputRequest]:
) -> AsyncIterator[ParsedJupyterMessage | InputRequest]:
"""Yield kernel messages while one cell executes."""
await self.start()
self._active_call_id = call_id
@@ -39,19 +39,22 @@ class JupyterShell:
kernel_call_id = await self.transport.execute(code)
async for message in self.transport.messages_for(kernel_call_id):
if isinstance(message, InputRequest):
yield replace(message, call_id=call_id)
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 = replace(message, parent_id=call_id)
if public_message.msg_type == "execute_reply":
self._last_execution_count = int(
public_message.content.get(
"execution_count", self._last_execution_count
)
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.get("status") == "error"
if public_message.content.status == "error"
else "ready"
)
self.last_used_at = datetime.now(UTC).isoformat()
+81 -31
View File
@@ -1,19 +1,22 @@
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
@dataclass
class KernelMessage:
"""A decoded Jupyter message kept at the transport boundary."""
msg_type: str
parent_id: str | None
content: dict[str, object]
buffers: list[bytes]
from .messages import (
ExecuteReplyMessage,
InputRequestMessage,
JupyterChannel,
JupyterMessage,
KernelMessage,
ParsedJupyterMessage,
StatusMessage,
parse_jupyter_message,
)
@dataclass
@@ -28,14 +31,21 @@ class InputRequest:
class JupyterTransport:
"""Own one persistent ipykernel subprocess and its message channels."""
def __init__(self) -> None:
_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[str, dict[str, Any] | BaseException]] | None
asyncio.Queue[tuple[JupyterChannel, dict[str, Any] | BaseException]] | None
) = None
self._reader_tasks: set[asyncio.Task[None]] = set()
@@ -44,7 +54,15 @@ class JupyterTransport:
if self.client is not None:
return
await self.manager.start_kernel()
# 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()
@@ -67,7 +85,7 @@ class JupyterTransport:
async def _read_channel(
self,
channel: str,
channel: JupyterChannel,
get_message: Callable[..., Awaitable[dict[str, Any]]],
) -> None:
"""Read one ZMQ channel continuously into the transport queue."""
@@ -106,7 +124,7 @@ class JupyterTransport:
async def messages_for(
self, call_id: str
) -> AsyncIterator[KernelMessage | InputRequest]:
) -> 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")
@@ -115,11 +133,11 @@ class JupyterTransport:
if self._message_queue is None:
raise RuntimeError("JupyterTransport message readers are not running")
reply: KernelMessage | None = None
reply: ExecuteReplyMessage | None = None
idle = False
try:
while True:
_, queued_message = await self._message_queue.get()
channel, queued_message = await self._message_queue.get()
if isinstance(queued_message, BaseException):
raise queued_message
@@ -128,37 +146,69 @@ class JupyterTransport:
if parent_id != call_id:
continue
if message["msg_type"] == "input_request":
decoded = parse_jupyter_message(message, channel=channel)
if isinstance(decoded, InputRequestMessage):
self._waiting_for_input = call_id
yield InputRequest(
call_id=call_id,
prompt=str(message.get("content", {}).get("prompt", "")),
password=bool(
message.get("content", {}).get("password", False)
),
prompt=decoded.content.prompt,
password=decoded.content.password,
)
continue
decoded = KernelMessage(
msg_type=message["msg_type"],
parent_id=parent_id,
content=dict(message.get("content", {})),
buffers=[bytes(buffer) for buffer in message.get("buffers", [])],
)
if decoded.msg_type == "execute_reply":
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 (
decoded.msg_type == "status"
and decoded.content.get("execution_state") == "idle"
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:
+7 -3
View File
@@ -7,7 +7,7 @@ from fastapi import Body, FastAPI, HTTPException, Path, Response
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from ipython_shell.jupyter import InputRequest, JupyterApp, KernelMessage
from ipython_shell.jupyter import InputRequest, JupyterApp, ParsedJupyterMessage
app = FastAPI(title="IPython Jupyter Web App", version="0.1.0")
shell_app = JupyterApp()
@@ -19,7 +19,7 @@ class InputReply(BaseModel):
value: str
def serialize_event(event: KernelMessage | InputRequest) -> dict[str, object]:
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 {
@@ -31,11 +31,15 @@ def serialize_event(event: KernelMessage | InputRequest) -> dict[str, object]:
# 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": event.content,
"content": content,
"buffers": [
base64.b64encode(buffer).decode("ascii") for buffer in event.buffers
],
+1
View File
@@ -0,0 +1 @@
"""Streamlit demonstration client for the Jupyter-backed shell."""
+312
View File
@@ -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()
+100
View File
@@ -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),
}
+12 -16
View File
@@ -4,7 +4,8 @@ import unittest
from ipython_shell.jupyter.app import JupyterApp
from ipython_shell.jupyter.shell import JupyterShell
from ipython_shell.jupyter.transport import InputRequest, KernelMessage
from ipython_shell.jupyter.messages import ExecuteResultMessage
from ipython_shell.jupyter.transport import InputRequest
class JupyterShellTests(unittest.IsolatedAsyncioTestCase):
@@ -30,19 +31,17 @@ class JupyterShellTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(
next(
message.content["data"]["text/plain"]
message.content.data["text/plain"]
for message in first
if isinstance(message, KernelMessage)
and message.msg_type == "execute_result"
if isinstance(message, ExecuteResultMessage)
),
"41",
)
self.assertEqual(
next(
message.content["data"]["text/plain"]
message.content.data["text/plain"]
for message in second
if isinstance(message, KernelMessage)
and message.msg_type == "execute_result"
if isinstance(message, ExecuteResultMessage)
),
"42",
)
@@ -97,19 +96,17 @@ class JupyterAppTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(
next(
message.content["data"]["text/plain"]
message.content.data["text/plain"]
for message in beta_view
if isinstance(message, KernelMessage)
and message.msg_type == "execute_result"
if isinstance(message, ExecuteResultMessage)
),
"False",
)
self.assertEqual(
next(
message.content["data"]["text/plain"]
message.content.data["text/plain"]
for message in alpha_view
if isinstance(message, KernelMessage)
and message.msg_type == "execute_result"
if isinstance(message, ExecuteResultMessage)
),
"'alpha'",
)
@@ -143,10 +140,9 @@ class JupyterAppTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(
next(
message.content["data"]["text/plain"]
message.content.data["text/plain"]
for message in messages
if isinstance(message, KernelMessage)
and message.msg_type == "execute_result"
if isinstance(message, ExecuteResultMessage)
),
"'Ada'",
)
+82
View File
@@ -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()
+132 -9
View File
@@ -1,16 +1,20 @@
import asyncio
import os
import unittest
from unittest.mock import patch
from ipython_shell.jupyter.transport import (
InputRequest,
JupyterTransport,
KernelMessage,
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 = KernelMessage(
message = JupyterMessage(
msg_type="display_data",
parent_id="call-1",
content={"data": {"text/plain": "42"}},
@@ -21,6 +25,46 @@ class TransportTests(unittest.TestCase):
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):
@@ -54,7 +98,7 @@ class AsyncTransportTests(unittest.IsolatedAsyncioTestCase):
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")
self.assertEqual(messages[-1].content.status, "ok")
async def test_transport_preserves_mime_and_errors(self):
transport = JupyterTransport()
@@ -80,12 +124,68 @@ class AsyncTransportTests(unittest.IsolatedAsyncioTestCase):
for message in display_messages
if message.msg_type == "display_data"
)
self.assertEqual(display.content["data"]["text/plain"], "hello")
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")
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()
@@ -101,7 +201,30 @@ class AsyncTransportTests(unittest.IsolatedAsyncioTestCase):
display = next(
message for message in messages if message.msg_type == "display_data"
)
self.assertIn("image/png", display.content["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()
Generated
+269
View File
@@ -1,6 +1,11 @@
version = 1
revision = 3
requires-python = ">=3.14"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform == 'emscripten'",
"sys_platform != 'emscripten' and sys_platform != 'win32'",
]
[[package]]
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" },
]
[[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]]
name = "annotated-doc"
version = "0.0.5"
@@ -93,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" },
]
[[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]]
name = "cachetools"
version = "7.1.7"
@@ -636,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" },
]
[[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]]
name = "httpx"
version = "0.28.1"
@@ -753,10 +805,16 @@ dependencies = [
{ name = "langchain-openai" },
{ name = "matplotlib" },
{ name = "matplotlib-inline" },
{ name = "pydantic" },
{ name = "uvicorn" },
{ name = "wonderwords" },
]
[package.dev-dependencies]
dev = [
{ name = "streamlit" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.141.1" },
@@ -768,10 +826,14 @@ requires-dist = [
{ name = "langchain-openai", specifier = ">=1.6.0" },
{ name = "matplotlib", specifier = ">=3.11.1" },
{ name = "matplotlib-inline", specifier = ">=0.1.7" },
{ name = "pydantic", specifier = ">=2.13.5" },
{ name = "uvicorn", specifier = ">=0.52.4" },
{ name = "wonderwords", specifier = ">=2.2.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "streamlit", specifier = ">=1.37" }]
[[package]]
name = "ipython-pygments-lexers"
version = "1.1.1"
@@ -784,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" },
]
[[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]]
name = "jaraco-classes"
version = "3.4.0"
@@ -838,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" },
]
[[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]]
name = "jiter"
version = "0.16.0"
@@ -1237,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" },
]
[[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]]
name = "matplotlib"
version = "3.11.1"
@@ -1325,6 +1438,15 @@ 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" },
]
[[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"
@@ -1486,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" },
]
[[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]]
name = "parso"
version = "0.8.7"
@@ -1587,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" },
]
[[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]]
name = "psutil"
version = "7.2.2"
@@ -1652,6 +1818,28 @@ memory = [
{ 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]]
name = "pycparser"
version = "3.0"
@@ -1736,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" },
]
[[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]]
name = "pygments"
version = "2.21.0"
@@ -2143,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" },
]
[[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]]
name = "tenacity"
version = "9.1.4"
@@ -2192,6 +2425,15 @@ 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" },
]
[[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"
@@ -2248,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" },
]
[[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]]
name = "uncalled-for"
version = "0.4.0"
@@ -2316,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" },
]
[[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]]
name = "watchfiles"
version = "1.2.0"