Skip to main content

Tokio-inspired Python JSON-RPC peers with named params

Project description

tw2_jsonrpc

tw2_jsonrpc is the Python sibling of the Rust tw2-jsonrpc experiment: one bidirectional JSON-RPC Peer, async-first transport code, named params only, and a small decorator API for registering local methods.

This package is peer-first: connect over TCP, attach to stdio, connect over WebSocket, launch a child process that speaks over stdio, or accept TCP and WebSocket peers with a server registry.

Client Call

import tw2_jsonrpc

async with await tw2_jsonrpc.connect_tcp("127.0.0.1", 8080) as peer:
    result = await peer.call("math.add", a=2, b=3)

Params are named only. Pass a dict or omit params:

await peer.call("math.add", {"a": 2, "b": 3})
await peer.call("math.add", a=2, b=3)
await peer.call("health")

Array params are rejected with InvalidParams.

Transports

All transports return a Peer. After that, calls and notifications feel the same.

peer = await tw2_jsonrpc.connect_tcp("127.0.0.1", 8080)
peer = await tw2_jsonrpc.connect_stdio()
peer = await tw2_jsonrpc.connect_ws("ws://127.0.0.1:8080/rpc")

Subprocesses

Use start_subprocess when the library should launch and own a child process that speaks newline-delimited JSON-RPC over stdio. This is stdio JSON-RPC with child-process lifecycle management, not a separate transport protocol:

peer = await tw2_jsonrpc.start_subprocess(["worker", "--stdio"])

start_process remains available as an alias.

Server

A Server owns methods that should be installed on every peer it creates. peer_cls is chosen at the transport boundary:

server = tw2_jsonrpc.Server()

@server.method("math.add")
async def add(a: int, b: int) -> int:
    return a + b

handle = await server.serve_tcp("127.0.0.1", 8080, peer_cls=tw2_jsonrpc.TcpPeer)
await handle.serve_forever()

Peer subclasses can still carry per-peer state and methods:

class AppPeer(tw2_jsonrpc.TcpPeer):
    @tw2_jsonrpc.method("session.ping")
    async def ping(self) -> str:
        return "pong"

handle = await server.serve_tcp("127.0.0.1", 8080, peer_cls=AppPeer)

Use a handler for per-connection setup:

async def on_peer(peer: AppPeer) -> None:
    @peer.method("session.echo")
    async def echo(text: str) -> str:
        return text

handle = await server.serve_tcp(
    "127.0.0.1",
    8080,
    peer_cls=AppPeer,
    handler=on_peer,
)

WebSocket serving and subprocess peers use the same server registry:

handle = await server.serve_ws("127.0.0.1", 8080, peer_cls=tw2_jsonrpc.WebSocketPeer)
peer = await server.connect_subprocess(["worker", "--stdio"], peer_cls=tw2_jsonrpc.SubprocessPeer)

The free serve_tcp and serve_ws helpers remain available when you do not need server-wide methods.

Peer Handlers

Runtime registration is available directly on a peer:

peer = await tw2_jsonrpc.connect_stdio()

@peer.method("math.add")
async def add(a: int, b: int) -> int:
    return a + b

The method name can default to the function name:

@peer.method
async def health() -> dict:
    return {"ok": True}

Peer subclasses can expose decorated methods:

class App(tw2_jsonrpc.StdioPeer):
    @tw2_jsonrpc.method("tools.echo")
    async def echo(self, text: str) -> str:
        return text

Subclass handlers receive self as the current peer, so callbacks and notifications can use the same object:

class App(tw2_jsonrpc.StdioPeer):
    @tw2_jsonrpc.method("callback.demo")
    async def callback_demo(self, name: str) -> dict:
        await self.notify("callback.ready", {"name": name})
        return {"ok": True}

You can also register or remove handlers explicitly:

peer.add_method(echo, "tools.echo")
peer.remove_method("tools.echo")

Notifications

Notifications are fire-and-forget. No result is returned.

await peer.notify("log.info", message="started")

Handlers live in the same method table. If the incoming JSON-RPC message omits id, no response is sent, even if the handler returns a value.

For readability, notification is available as an alias for method registration:

@peer.notification("log.info")
async def log_info(message: str) -> None:
    print(message)

Errors

Remote JSON-RPC error responses become JsonRpcError subclasses:

try:
    result = await peer.call("math.add", {"a": 1, "b": 2})
except tw2_jsonrpc.InvalidParams:
    ...
except tw2_jsonrpc.JsonRpcError:
    ...

Handlers can raise structured JSON-RPC errors:

raise tw2_jsonrpc.InvalidParams("field 'name' is required")

Local timeout and cancellation stay local:

result = await peer.call("slow.method", {"x": 1}, timeout=5.0)

If a call is cancelled or times out, the pending local waiter is removed. JSON-RPC 2.0 has no standard remote cancellation, so a later response for that id is ignored.

Mental Model

  • Peer is a bidirectional JSON-RPC connection.
  • await peer.call(...) sends a request and returns the decoded result.
  • await peer.notify(...) sends a fire-and-forget message.
  • @peer.method registers a request handler.
  • Server registers methods shared by every peer it accepts or starts.
  • @peer.notification is a readability alias for notification handlers.
  • Only named params are supported.
  • The shape is intentionally close to tw2-jsonrpc, but Python does not need a builder or proc macro.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tw2_jsonrpc-1.0.1.tar.gz (8.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tw2_jsonrpc-1.0.1-py3-none-any.whl (12.1 kB view details)

Uploaded Python 3

File details

Details for the file tw2_jsonrpc-1.0.1.tar.gz.

File metadata

  • Download URL: tw2_jsonrpc-1.0.1.tar.gz
  • Upload date:
  • Size: 8.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tw2_jsonrpc-1.0.1.tar.gz
Algorithm Hash digest
SHA256 f57ed9c105bc8653b16b54cb75f8c677cbcacc009fe0544958b0e170045daa29
MD5 dc25c47746cae64872d3c3fcaf03ac94
BLAKE2b-256 f482587146dc9b1805cbd562b4463ee4088761c95b29d0f56a7d604e01ae18e2

See more details on using hashes here.

File details

Details for the file tw2_jsonrpc-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: tw2_jsonrpc-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 12.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tw2_jsonrpc-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 be36ddb9c294db5ca5c04d674c8f169b33f08f8d2ff8eca45e2c29b3121eb895
MD5 1fa30f63cdcb5933f1948410329d6087
BLAKE2b-256 7058f0092ba04e7c561941f482f3b62b2bb7c27efbdae47cc058aea4f970e4c8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page