Websocket Client
A pool of remote monty workers reached over a WebSocket instead of local subprocesses — the intended peer is
monty-server.
See running monty-server.
Async context manager owning a pool of remote monty workers reached over a
WebSocket. The intended peer is monty-server (the production server: one
monty subprocess child per connection, plus capacity/timeout policy and
graceful drain), but any server that accepts the connection and bridges to
a worker fits — a relay pairing it with a child that dialed in from the
other end, or the dev relay scripts/websocket_relay.py.
Like AsyncMonty, but instead of spawning local subprocesses each checkout
dials the configured URL. There is no sync counterpart — remote turns are
network-bound. checkout() yields the same AsyncMontySession.
A monty-server enforces its own policy on top of the pool’s. On SIGTERM
drain it answers the session’s next request with MontyShutdown, whose
dump restores the session onto another server; every other server-side
drop (idle, session or turn timeout, capacity) closes the connection and
raises MontyDisconnectError.
async with AsyncMontyWebsocket('ws://127.0.0.1:8799') as pool:
async with pool.checkout() as session:
result = await session.feed_run('1 + 1')
def __new__(
cls,
url: str,
*,
max_processes: int | None = None,
checkout_timeout: float | None = None,
request_timeout: float | None = 10.0,
connect_headers: Callable[[], Mapping[str, str]] | None = None,
) -> Self
Configure a remote worker pool; connections are made by async with and
each checkout (no workers are pre-warmed).
url : str
ws:///wss:// URL to dial — a relay, or any server that
bridges to a worker. Dialed verbatim; any session/rendezvous routing the URL
needs (e.g. a /<uuid>/parent path for a relay) must already be
in it.
Cap on concurrent connections (defaults to the CPU count); checkouts beyond it wait.
Seconds checkout() waits for capacity before
raising TimeoutError. None waits forever.
Hard per-turn deadline in seconds (default 10.0) — a
worker that exceeds it has its connection killed and the call
raises MontyCrashedError with timed_out=True. This also
bounds the wait when a relay accepts the connection but never
produces a worker. Pass None to wait indefinitely.
Note that install_dependencies is a turn too, so the default
10.0 is often too low for it — a real uv pip install can exceed
it. Raise request_timeout (or pass None) when installing
dependencies over the WebSocket transport.
Called once per session, as it is entered and before
any wait for pool capacity, to produce extra str to str
headers for that connection’s WebSocket upgrade request — e.g. a
token for infrastructure in front of the worker. It runs
synchronously on the checking-out task, so it sees that task’s
contextvars and must not block. Monty never interprets the
values; duplicate names are last-wins, also over the default
user-agent and the traceparent the Logfire integration
adds, and a malformed name or value raises RuntimeError as
the session is entered.
def checkout(
*,
script_name: str = 'main.py',
limits: ResourceLimits | None = None,
type_check: bool = False,
type_check_stubs: str | None = None,
type_check_format: TypeCheckFormat | None = None,
type_check_color: bool = False,
assert_message_annotations: bool | int = ...,
print_flush_interval: float | None = None,
) -> AsyncMontySession
Prepare a REPL session served by a dedicated remote connection.
Identical to AsyncMonty.checkout; the connection is opened by
async with on the returned session.