Skip to main content

Wirth-style minimal SSE web framework with topic-scoped pub/sub

Project description

py_sse

A small Python web framework for real-time hypermedia apps. HTTP/1.1, one OS thread per connection, brotli-compressed SSE, topic-scoped pub/sub. No async. About 1200 lines.

Pairs with html-tags and Datastar.

Install

pip install py-sse

Hello, live page

from py_sse import serve, live, Database, signals
from html_tags import h

db = Database("app.db", schema="CREATE TABLE IF NOT EXISTS msg (text TEXT)")

@live(topic="chat")
def home(req):
    msgs = db.all("SELECT text FROM msg")
    return [
        h.h1("chat"),
        h.ul(*[h.li(t) for (t,) in msgs]),
        h.input({"data-bind:text": "",
                 "data-on:keydown": "evt.key === 'Enter' && "
                                    "($text && (@post('/say'), $text=''))"}),
    ]

def say(req):
    text = (signals(req).get("text") or "").strip()
    if text:
        db.execute("INSERT INTO msg (text) VALUES (?)", (text,))
        db.changes.notify("chat")
    return (200, [], b"")

ROUTES = [("GET", "/", home), ("POST", "/say", say)]

if __name__ == "__main__":
    serve(ROUTES, port=8000, changes=db.changes, head=[
        h.title("chat"),
        h.script(type="module", src="https://cdn.jsdelivr.net/gh/"
                 "starfederation/datastar@v1.0.1/bundles/datastar.js"),
    ])

Open two browser tabs. Type in one, watch it appear in the other.

How it works

  • One URL per live page. @live(topic="x") makes the handler serve three transport modes from the same route, dispatched by Accept:

    • Live SSE while viewers are under soft_cap.
    • Polling between soft_cap and hard_cap (interval ramps exponentially).
    • Static above hard_cap (page stops auto-updating).
    • Polling clients re-upgrade to live when capacity drops.
  • CQRS. Reads come down the long-lived SSE stream. Writes are short-lived @post() calls that mutate state and call db.changes.notify("topic"). All open streams re-render.

  • Topic patterns. Subjects are dotted: "game.42.score". A waiter on "game.42.*" wakes on any of game.42.score, game.42.players, etc. @live(topic="game.{id}.*") expands {id} from URL parameters. changes.wait([p1, p2, ...]) waits on multiple patterns at once.

  • Fat morph. Every frame is the whole page. idiomorph diffs in place. Brotli's cross-frame state compresses repeated HTML at ~200:1.

  • Framework owns the envelope. Pass head=[...] to serve() once. The <html><head><body> is byte-identical on every render, so idiomorph leaves it alone and scripts stay running across morphs.

API

serve(routes, *, host, port, changes, head, capacity, on_event,
      before_hooks, ui_theme, max_connections, access_log)

live(handler, topic=..., hard_cap=...)  # function or @live(topic="...")

Database(path, schema)                  # apsw passthrough + .changes
Changes()                               # topic-scoped pub/sub
  .notify(subject)                      # publish
  .wait(pattern, timeout=...)           # pattern: str | iterable of str
Capacity(soft_cap, hard_cap, ...)       # viewer capacity

html(body), redirect(loc), error(status, msg), no_content()
signals(req)                            # parse Datastar signals
set_cookie(req, name, value, **opts)

Handlers return html_tags Node, Safe, or an iterable of those. Writes return a (status, headers, body) tuple.

on_event(event_dict) is an optional callback fired on stream_open, stream_close, and page_render. No-op by default.

Status

0.8.0. Small, opinionated, not yet hardened for hostile traffic. Pair with a reverse proxy for TLS. Compression and SSE buffering are already correct on the application side.

MIT.

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

py_sse-0.9.0.tar.gz (15.6 kB view details)

Uploaded Source

Built Distribution

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

py_sse-0.9.0-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file py_sse-0.9.0.tar.gz.

File metadata

  • Download URL: py_sse-0.9.0.tar.gz
  • Upload date:
  • Size: 15.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux ARM","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 py_sse-0.9.0.tar.gz
Algorithm Hash digest
SHA256 06df4e7cf521e98543043bd752ceb83131e7384b7d59dcdb925e7408a418c128
MD5 829d264220194958a840c30ddd348ebf
BLAKE2b-256 bbde9ba47977fd07e7123f0bc7c4c62006113301056f19a90d69a34ad245b74a

See more details on using hashes here.

File details

Details for the file py_sse-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: py_sse-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux ARM","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 py_sse-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1fd13c893ec43cd05c26348bc74ade8168963a098aa371f04f53c75d4c685977
MD5 8f03884cfef5dc59bdc3220b2db8f190
BLAKE2b-256 3e94163a34688e6befb55c99946d812abe875f2f78a742e78c7931fbb3ff2a88

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