Skip to main content

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

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 with ctx.req (the raw request), ctx.opts (server options), and ctx.wsa (the server instance)
  • q — the parameters as a propertybag.Bag (dot-accessible dict)
  • Return a dict and 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 from
  • defpage — 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

webshoes-1.0.0.tar.gz (24.6 kB view details)

Uploaded Source

Built Distribution

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

webshoes-1.0.0-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

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

Hashes for webshoes-1.0.0.tar.gz
Algorithm Hash digest
SHA256 cc0626031fc8e959745be27a6dfa4a8e6760507701fddaceffcbcd7281d3e482
MD5 89647eaf77a938257572a7654c6fe858
BLAKE2b-256 c15feb39a3d8ed926f40ae03cc4e661eed336460b905961edd15505921a06fa6

See more details on using hashes here.

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

Hashes for webshoes-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e55a2c306d5d654e25e3b9e1b5234fd17ea17aa06e0b467c30750b2de9556016
MD5 3a220b69a955df75416e6f8943532277
BLAKE2b-256 8362ccd2888cce119c373e10f82f2b4cc22eaa84d47996cd6bb23289253fe154

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