Quick start¶
Expose methods at /ws/, connect once, then call in either direction.
Choose your framework below. Installation, method and server tabs stay in sync;
the Python and browser clients work with every server shown here.
Install¶
Use Python 3.10+ in a virtual environment. Install the package for your server:
pip install wsrpc-aiohttp
pip install 'wsrpc-asgi[client,fastapi]' uvicorn
pip install 'wsrpc-asgi[client,starlette]' uvicorn
pip install 'wsrpc-asgi[client]' django uvicorn
pip install 'wsrpc-asgi[client]' django channels uvicorn
pip install 'wsrpc-asgi[client]' quart uvicorn
pip install 'wsrpc-asgi[client]' litestar uvicorn
pip install 'wsrpc-asgi[client]' falcon uvicorn
pip install 'wsrpc-asgi[client]' sanic uvicorn
pip install 'wsrpc-asgi[client]' blacksheep uvicorn
pip install 'wsrpc-asgi[client]' uvicorn
For a source installation of the ASGI library, use the same extras with its Git repository instead of the distribution name, for example:
pip install 'wsrpc-asgi[client,fastapi] @ git+https://github.com/wsrpc/wsrpc-asgi.git' uvicorn
Define the RPC methods¶
Every server below exposes the same two methods: echo returns its keyword
arguments, and callback asks the client to double a value.
The method definitions below follow your framework selection.
These methods are included in the complete app.py server below.
from wsrpc_aiohttp import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
These methods are included in the complete app.py server below.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
Connect your server¶
Save the selected server as app.py. Every tab is a complete, standalone
file containing its RPC methods and framework integration. Start the server with the command in the same tab.
All examples listen at ws://127.0.0.1:8000/ws/; all except bare ASGI also
serve GET /health as an ordinary HTTP route.
from wsrpc_aiohttp import WebSocketAsync
from aiohttp import web
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
async def health(request):
return web.json_response({"ok": True})
def create_app():
app = web.Application()
app.router.add_get("/ws/", RPC)
app.router.add_get("/health", health)
return app
if __name__ == "__main__":
web.run_app(create_app(), host="127.0.0.1", port=8000)
python app.py
serve() accepts an unaccepted FastAPI WebSocket. Dependencies run once per connection; their results are available through socket.context.
from fastapi import Depends, FastAPI, WebSocket
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
app = FastAPI()
async def connection_context(websocket: WebSocket):
return {"client": websocket.client.host if websocket.client else None}
@app.websocket("/ws/")
async def rpc(websocket: WebSocket, context=Depends(connection_context)):
await RPC.serve(websocket, context=context)
@app.get("/health")
async def health():
return {"ok": True}
uvicorn app:app --host 127.0.0.1 --port 8000
Keep the endpoint and close it in the application’s lifespan, as shown here.
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route, WebSocketRoute
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
rpc = RPC.as_asgi()
@asynccontextmanager
async def lifespan(app):
yield
await rpc.close()
async def health(request):
return JSONResponse({"ok": True})
app = Starlette(
routes=[Route("/health", health), WebSocketRoute("/ws/", rpc)],
lifespan=lifespan,
)
uvicorn app:app --host 127.0.0.1 --port 8000
This standalone example configures minimal settings. In an existing project keep your DJANGO_SETTINGS_MODULE. The RPC wrapper bypasses Django HTTP middleware; authorize WebSockets separately.
import django
from django.conf import settings
from django.core.asgi import get_asgi_application
from django.http import JsonResponse
from django.urls import path
from wsrpc_asgi import RPCMiddleware, WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
# In an existing project keep DJANGO_SETTINGS_MODULE and its settings.
if not settings.configured:
settings.configure(
DEBUG=True,
SECRET_KEY="local-example-only",
ROOT_URLCONF=__name__,
ALLOWED_HOSTS=["*"],
)
django.setup()
def health(request):
return JsonResponse({"ok": True})
urlpatterns = [path("health", health)]
http_app = get_asgi_application()
rpc = RPC.as_asgi()
app = RPCMiddleware(http_app, rpc, path="/ws/")
uvicorn app:app --host 127.0.0.1 --port 8000
This standalone file includes Django settings, the HTTP application and the Channels router. In an existing Channels project retain its settings and authentication/origin middleware.
import django
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf import settings
from django.core.asgi import get_asgi_application
from django.http import JsonResponse
from django.urls import path
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
# In an existing project keep DJANGO_SETTINGS_MODULE and its settings.
if not settings.configured:
settings.configure(
DEBUG=True,
SECRET_KEY="local-example-only",
ROOT_URLCONF=__name__,
ALLOWED_HOSTS=["*"],
)
django.setup()
def health(request):
return JsonResponse({"ok": True})
urlpatterns = [path("health", health)]
http_app = get_asgi_application()
rpc = RPC.as_asgi()
app = ProtocolTypeRouter(
{
"http": http_app,
"websocket": URLRouter([path("ws/", rpc)]),
}
)
uvicorn app:app --host 127.0.0.1 --port 8000
from quart import Quart
from wsrpc_asgi import RPCMiddleware, WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
app = Quart(__name__)
@app.get("/health")
async def health():
return {"ok": True}
rpc = RPC.as_asgi()
app.asgi_app = RPCMiddleware(app.asgi_app, rpc, path="/ws/")
uvicorn app:app --host 127.0.0.1 --port 8000
from litestar import Litestar, get
from wsrpc_asgi import RPCMiddleware, WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
@get("/health")
async def health() -> dict[str, bool]:
return {"ok": True}
http_app = Litestar(route_handlers=[health])
rpc = RPC.as_asgi()
app = RPCMiddleware(http_app, rpc, path="/ws/")
uvicorn app:app --host 127.0.0.1 --port 8000
import falcon.asgi
from wsrpc_asgi import RPCMiddleware, WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
class Health:
async def on_get(self, request, response):
response.media = {"ok": True}
http_app = falcon.asgi.App()
http_app.add_route("/health", Health())
rpc = RPC.as_asgi()
app = RPCMiddleware(http_app, rpc, path="/ws/")
uvicorn app:app --host 127.0.0.1 --port 8000
Run through Uvicorn in ASGI mode so the RPC wrapper receives connections.
from sanic import Sanic
from sanic.response import json
from wsrpc_asgi import RPCMiddleware, WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
http_app = Sanic("wsrpc_example")
@http_app.get("/health")
async def health(request):
return json({"ok": True})
rpc = RPC.as_asgi()
app = RPCMiddleware(http_app, rpc, path="/ws/")
uvicorn app:app --host 127.0.0.1 --port 8000
from blacksheep import Application
from blacksheep.server.responses import json
from wsrpc_asgi import RPCMiddleware, WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
http_app = Application()
@http_app.router.get("/health")
async def health():
return json({"ok": True})
rpc = RPC.as_asgi()
app = RPCMiddleware(http_app, rpc, path="/ws/")
uvicorn app:app --host 127.0.0.1 --port 8000
The endpoint itself is an ASGI callable. Use an asyncio server backend.
from wsrpc_asgi import WebSocketAsync
class RPC(WebSocketAsync):
pass
async def echo(socket, **params):
return params
async def callback(socket, *, value):
return await socket.proxy.double(value=value)
RPC.add_route("echo", echo)
RPC.add_route("callback", callback)
rpc = RPC.as_asgi(path="/ws/")
app = rpc
uvicorn app:app --host 127.0.0.1 --port 8000
Each handler receives the current RPC connection as its first argument. Expose only the methods you intend peers to call. See ASGI integration details for authentication, lifecycle, server backends and deployment; aiohttp routes cover aiohttp-specific handler behavior.
Call from Python¶
Use either Python client with any server above. Save as client.py,
then run python client.py while the server is running. The callback is
registered before connecting, so the server can call it immediately.
import asyncio
from wsrpc_aiohttp import WSRPCClient
async def main():
async with WSRPCClient("ws://127.0.0.1:8000/ws/", timeout=5) as client:
client.add_route("double", lambda socket, value: value * 2)
print(await client.proxy.echo(message="Hello"))
print(await client.proxy.callback(value=21))
if __name__ == "__main__":
asyncio.run(main())
Expected output:
{'message': 'Hello'}
42
import asyncio
from wsrpc_asgi import WSRPCClient
async def main():
async with WSRPCClient("ws://127.0.0.1:8000/ws/", timeout=5) as client:
client.add_route("double", lambda socket, value: value * 2)
print(await client.proxy.echo(message="Hello"))
print(await client.proxy.callback(value=21))
if __name__ == "__main__":
asyncio.run(main())
Expected output:
{'message': 'Hello'}
42
Install wsrpc-aiohttp for the aiohttp client, or wsrpc-asgi[client] for
the websockets client. Calls use keyword arguments. The context manager
connects on entry and closes the connection on exit. Use wss:// with TLS.
Call from a browser¶
Both packages include the same JavaScript client. Serve STATIC_DIR at
/js/ using your framework’s static-file support, then use the same code
with every server:
<script src="/js/wsrpc.min.js"></script>
<script>
const scheme = location.protocol === "https:" ? "wss:" : "ws:";
const rpc = new WSRPC(`${scheme}//${location.host}/ws/`);
rpc.addRoute("double", ({ value }) => value * 2);
rpc.connect();
async function run() {
await rpc.onEvent("onconnect");
try {
console.log(await rpc.call("echo", { message: "Hello" }));
console.log(await rpc.call("callback", { value: 21 }));
} catch (error) {
console.error(error);
}
}
run();
</script>
For complete demos that serve both the page and JavaScript, see the aiohttp browser demo and FastAPI browser demo. Continue with clients, the protocol or API differences.