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.10.0a4.tar.gz (12.3 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.10.0a4-py3-none-any.whl (15.5 kB view details)

Uploaded Python 3

File details

Details for the file py_sse-0.10.0a4.tar.gz.

File metadata

  • Download URL: py_sse-0.10.0a4.tar.gz
  • Upload date:
  • Size: 12.3 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.10.0a4.tar.gz
Algorithm Hash digest
SHA256 ec918fbf1d5f56acff76386d754c4a35263c5b6c4175f90af6541e1de0570fcd
MD5 c1d01063ad8ff728231f233c895226e7
BLAKE2b-256 21334f5b62bc121a5bacecea7938975213023b12d8764c0de39ed2317ec30603

See more details on using hashes here.

File details

Details for the file py_sse-0.10.0a4-py3-none-any.whl.

File metadata

  • Download URL: py_sse-0.10.0a4-py3-none-any.whl
  • Upload date:
  • Size: 15.5 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.10.0a4-py3-none-any.whl
Algorithm Hash digest
SHA256 3a6d6f478f107b4f12e5947ba343a645f7221163576760dac7760b20d93e4241
MD5 d35f43ef611c070106885a21654ce440
BLAKE2b-256 df4f341a2f988e85d2d0a4e616f376f2cc24041d5ba8895695d844d224c2e73d

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