Quickly create a web service supporting both websockets and http.
Project description
webshoes
Quickly create a web service that handles both HTTP and WebSocket connections with a single unified handler registration.
import webshoes as ws
def cmdAdd(ctx, q):
return {'result': int(q.a) + int(q.b)}
wsa = ws.WebShoesApp('127.0.0.1', 15801, {'verbose': True})
wsa.register('cmd', 'cmd', 'q', 'evt', 'r', {'add': cmdAdd})
wsa.start()
Table of contents
- Install
- How it works
- Registering handlers
- HTTP requests
- WebSocket requests
- Event system
- Static file serving
- JavaScript client
- Running the demos
- Running tests
- Building and publishing
Install
From PyPI:
pip3 install webshoes
Local development install:
To install from source so that changes to the code take effect immediately without reinstalling:
pip3 install -e .
This creates a live link into your local source tree. import webshoes will always load from ./webshoes/ in the project directory.
To uninstall:
pip3 uninstall webshoes
How it works
webshoes runs an async HTTP server (via aiohttp) that handles both HTTP and WebSocket connections on the same port. You register named handler functions, and the library routes incoming requests to the right function — whether they arrive over HTTP or WebSocket.
The same Python function works for both transports:
def cmdAdd(ctx, q):
return {'result': int(q.a) + int(q.b)}
ctx— context object withctx.req(the raw request),ctx.opts(server options), andctx.wsa(the server instance)q— the parameters as apropertybag.Bag(dot-accessible dict)- Return a
dictand it's sent back as JSON automatically
Registering handlers
wsa.register(sub, cmd, q, evt, rep, fm)
| Parameter | Description |
|---|---|
sub |
HTTP path prefix. Requests to /sub/funcname are routed here. Use '*' to match any prefix. |
cmd |
WebSocket field name that contains the command/function name |
q |
WebSocket field name that contains the function arguments. Use '' to read arguments from the top-level message object. |
evt |
WebSocket field name used to subscribe to server-push events |
rep |
Reply field name that wraps the return value. Use '' to merge return values into the top-level reply. |
fm |
Dict mapping function names to callables. Use '*' as a key to catch any unmatched function name. |
Example:
import webshoes as ws
def cmdHeartbeat(ctx, q):
return {'status': 'ok'}
def cmdAdd(ctx, q):
return {'result': int(q.a) + int(q.b)}
def cmdCatchAll(ctx, q):
return {'path': ctx.p, 'args': q.as_dict()}
wsa = ws.WebShoesApp('127.0.0.1', 15801, {'verbose': True})
wsa.register('cmd', 'cmd', 'q', 'evt', 'r', {
'heartbeat': cmdHeartbeat,
'add': cmdAdd,
'*': cmdCatchAll, # catches any unmatched command
})
wsa.start()
HTTP requests
HTTP requests are routed by URL path: GET /sub/funcname?param=value
import requests
# Calls cmdHeartbeat
r = requests.get('http://127.0.0.1:15801/cmd/heartbeat')
print(r.json()) # {'status': 'ok'}
# Calls cmdAdd with a=3, b=4
r = requests.get('http://127.0.0.1:15801/cmd/add?a=3&b=4')
print(r.json()) # {'result': 7}
POST parameters are also supported and merged with query string parameters.
WebSocket requests
WebSocket messages are JSON objects. The command value must include the sub prefix registered with register(), followed by the function name: sub/funcname.
import asyncio, json, websockets
async def main():
async with websockets.connect('ws://127.0.0.1:15801') as wsc:
# Call heartbeat — 'cmd' is the sub name, 'heartbeat' is the function
await wsc.send(json.dumps({'cmd': 'cmd/heartbeat'}))
reply = json.loads(await wsc.recv())
print(reply) # {'r': {'status': 'ok'}, 't': ...}
# Call add with arguments in the 'q' field
await wsc.send(json.dumps({'cmd': 'cmd/add', 'q': {'a': 3, 'b': 4}}))
reply = json.loads(await wsc.recv())
print(reply['r']['result']) # 7
asyncio.run(main())
If the q field is absent, arguments are read from the top level of the message instead:
await wsc.send(json.dumps({'cmd': 'cmd/add', 'a': '3', 'b': '4'}))
Transaction IDs: Include a tid field in the request and it will be echoed back in the reply, useful for matching responses to requests on the client side.
await wsc.send(json.dumps({'cmd': 'cmd/heartbeat', 'tid': 'abc123'}))
reply = json.loads(await wsc.recv())
print(reply['tid']) # 'abc123'
Event system
The server can push data to connected clients whenever something changes. Clients subscribe by sending a message with the evt field, and the server pushes updates when triggerEvent() is called.
Server side — push an event:
wsa.triggerEvent('sensorUpdate', {'temperature': 72.3})
Client side — subscribe and receive:
async with websockets.connect('ws://127.0.0.1:15801') as wsc:
# Subscribe to the event
await wsc.send(json.dumps({'evt': 'sensorUpdate'}))
ack = json.loads(await wsc.recv())
print(ack['r']['uid']) # assigned uid for this subscription
# Receive pushed event data
push = json.loads(await wsc.recv())
print(push['r']) # {'temperature': 72.3}
Events are only pushed when the data changes (version-based). If a client subscribes after an event has already fired, it receives the most recent value immediately on the next trigger.
Static file serving
To serve static files from a directory, pass a dict instead of a callable in the function map:
wsa.register('*', '', '', '', '', {
'*': {'root': '/path/to/static/files', 'defpage': 'index.html'}
})
root— directory to serve files fromdefpage— default file to redirect to when a directory is requested
JavaScript client
A browser-side WebSocket client is included at webshoes/web/webshoes.js. It handles connection management and auto-reconnects.
let wsc = WebShoes({
url: 'ws://127.0.0.1:15801',
cb: (type, data) => console.log(type, data)
});
// Send a command
wsc.msg({cmd: 'add', q: {a: 3, b: 4}}, (reply) => {
console.log(reply.r.result); // 7
});
// Subscribe to a server-push event
wsc.onEvent('sensorUpdate', (data) => {
console.log(data.r); // {temperature: 72.3}
});
Running the demos
Three demos are included in the demo/ directory.
demo/basic/basic.py — basic demo
Shows the simplest usage: starts a server, registers an add function, calls it over both HTTP and WebSocket, prints the results, then shuts down.
python3 demo/basic/basic.py
Expected output:
--- HTTP ---
GET /cmd/add?a=2&b=3 → {"result": 5}
POST /cmd/add {a:10, b:20} → {"result": 30}
--- WebSocket ---
cmd/add {a:2, b:3} → {'r': {'result': 5}, 't': ...}
cmd/add with tid → {'r': {'result': 9}, 'tid': 'req-001', 't': ...}
demo/grid/grid.py — interactive web UI demo
A more complete example: serves a 10×10 clickable grid in the browser. Clicking a cell increments its value (0–9) and pushes the new grid state to all connected clients via server-push events. Demonstrates static file serving, async handlers, and real-time events.
python3 demo/grid/grid.py
Then open the URL printed to the console:
http://127.0.0.1:12909/site/squares.html
Multiple browser tabs can connect simultaneously — clicking in one tab updates all others in real time.
demo/canvas/canvas.py — shared drawing canvas demo
A shared 500×500 drawing canvas served in the browser. All connected clients draw on the same canvas in real time. Tools: pen with colour picker, eraser, clear, and save as PNG. A live stroke counter shows how many strokes are stored on the server. Demonstrates real-time event broadcasting and canvas state replay for late-joining clients.
python3 demo/canvas/canvas.py
Then open the URL printed to the console:
http://127.0.0.1:12910/site/canvas.html
Open in multiple tabs to draw collaboratively — strokes from one tab appear instantly in all others.
Make sure the dependencies are installed before running any demo:
pip3 install aiohttp threadmsg propertybag sparen requests websockets
Running tests
pytest test/
The test suite covers HTTP, WebSocket, events, static file serving, registration, and server lifecycle across 63 tests. It starts isolated server instances on dedicated ports and cleans them up automatically.
Install pytest and the test dependencies first:
pip3 install pytest aiohttp threadmsg propertybag sparen requests websockets
Run with verbose output to see each test name:
pytest test/ -v
Building and publishing
1. Install the build tools (once):
pip3 install build twine
2. Bump the version in webshoes/PROJECT.txt — PyPI rejects uploads for a version that already exists.
3. Build the package:
python3 -m build
This creates a dist/ folder containing a .tar.gz (source distribution) and a .whl (wheel).
4. Upload to PyPI:
twine upload dist/*
Twine will prompt for your PyPI username and password. The recommended approach is to use an API token: enter __token__ as the username and your token as the password.
Optional — test the upload first using TestPyPI before publishing publicly:
twine upload --repository testpypi dist/*
Optional — save credentials to avoid being prompted each time by creating ~/.pypirc:
[pypi]
username = __token__
password = pypi-your-token-here
Comparison to similar projects
python-socketio
python-socketio is the closest match in terms of goals: named event handlers, server-push events, and support for both HTTP and WebSocket. It is mature, widely used, and has a large ecosystem.
Key differences:
- python-socketio uses the Socket.IO protocol, which is a custom framing layer on top of WebSocket. This means clients must use a Socket.IO client library rather than a raw WebSocket. webshoes uses plain WebSocket and plain HTTP with no custom protocol.
- python-socketio has significantly more features: rooms, namespaces, broadcasting, multiple transport fallbacks (long-polling), and official client libraries for many languages.
- webshoes routes a single registered handler to both HTTP and WebSocket automatically. python-socketio treats HTTP and WebSocket as separate concerns.
Choose python-socketio if: you need broad client compatibility, fallback transports, rooms/namespaces, or official clients for mobile/other platforms.
Choose webshoes if: you want plain WebSocket and HTTP with no custom protocol overhead, or your clients are already using raw WebSocket.
Flask-SocketIO
Flask-SocketIO is a Flask extension built on top of python-socketio. Everything above about python-socketio applies here as well.
Key differences:
- Tightly coupled to Flask, so it inherits Flask's routing, request context, and extension ecosystem.
- Flask is a synchronous framework by default; async support requires extra configuration.
- webshoes is async-native (built on aiohttp).
Choose Flask-SocketIO if: you are already building a Flask application and want to add real-time WebSocket functionality with minimal restructuring.
Choose webshoes if: you are starting fresh and want a lightweight, async-native server without the Flask dependency.
FastAPI
FastAPI is a full-featured modern web framework for building HTTP APIs, with WebSocket support added alongside HTTP. It is one of the most popular Python web frameworks currently in active development.
Key differences:
- FastAPI has a much larger feature set: automatic OpenAPI/Swagger documentation, Pydantic-based request validation, dependency injection, OAuth2, and extensive middleware support.
- HTTP and WebSocket handlers are separate in FastAPI — there is no single function that automatically serves both transports.
- FastAPI has a large community, extensive documentation, and is widely used in production.
- webshoes has far fewer dependencies and less boilerplate for simple use cases.
Choose FastAPI if: you are building a production API where validation, auto-generated docs, or a rich middleware ecosystem matter, or if you expect the project to grow significantly in scope.
Choose webshoes if: you need a minimal service where the same logic should be reachable over HTTP and WebSocket with very little setup.
Starlette
Starlette is the lightweight ASGI framework that FastAPI is built on. It handles both HTTP and WebSocket at a lower level than FastAPI but requires more manual wiring.
Key differences:
- Starlette gives you full control over routing and middleware but provides no automatic handler registration or event system.
- It is closer in abstraction level to aiohttp (which webshoes uses internally) than to webshoes itself.
- Starlette is a building block; webshoes is an end-user API.
Choose Starlette if: you want a flexible async foundation to build your own conventions on top of, or you are building a framework layer yourself.
Choose webshoes if: you want the conventions already decided and a working HTTP+WebSocket server with minimal code.
Summary table
| webshoes | python-socketio | Flask-SocketIO | FastAPI | Starlette | |
|---|---|---|---|---|---|
| HTTP support | Yes | Partial | Yes (via Flask) | Yes | Yes |
| WebSocket support | Yes | Yes | Yes | Yes | Yes |
| Same handler for both transports | Yes | No | No | No | No |
| Server-push events | Yes | Yes | Yes | No (manual) | No (manual) |
| Raw WebSocket protocol | Yes | No (Socket.IO) | No (Socket.IO) | Yes | Yes |
| Request validation | No | No | No | Yes (Pydantic) | No |
| Auto-generated API docs | No | No | No | Yes | No |
| Async-native | Yes | Yes | Optional | Yes | Yes |
| Relative complexity | Low | Medium | Medium | High | Medium |
References
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file webshoes-1.0.0.tar.gz.
File metadata
- Download URL: webshoes-1.0.0.tar.gz
- Upload date:
- Size: 24.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc0626031fc8e959745be27a6dfa4a8e6760507701fddaceffcbcd7281d3e482
|
|
| MD5 |
89647eaf77a938257572a7654c6fe858
|
|
| BLAKE2b-256 |
c15feb39a3d8ed926f40ae03cc4e661eed336460b905961edd15505921a06fa6
|
File details
Details for the file webshoes-1.0.0-py3-none-any.whl.
File metadata
- Download URL: webshoes-1.0.0-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e55a2c306d5d654e25e3b9e1b5234fd17ea17aa06e0b467c30750b2de9556016
|
|
| MD5 |
3a220b69a955df75416e6f8943532277
|
|
| BLAKE2b-256 |
8362ccd2888cce119c373e10f82f2b4cc22eaa84d47996cd6bb23289253fe154
|