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.0a2.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.0a2-py3-none-any.whl (15.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: py_sse-0.10.0a2.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.0a2.tar.gz
Algorithm Hash digest
SHA256 8cad233347c792962a6e4e77adb48e2c03be3f7ada5182a4409e7635d2a1783f
MD5 eaaa82510a50a9ddf053467ee718c3a4
BLAKE2b-256 846d72f3d3f8ccc76cd15ac4a3eed5dc07df82d1c1ec51ddbc2adc0d8b4f89b5

See more details on using hashes here.

File details

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

File metadata

  • Download URL: py_sse-0.10.0a2-py3-none-any.whl
  • Upload date:
  • Size: 15.3 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.0a2-py3-none-any.whl
Algorithm Hash digest
SHA256 38c0b938f37ce4a7e9584ec47f2f5be295b32919dc0781c9682e6bc28084627d
MD5 7dba76060188c18dae7497fb117e9e8a
BLAKE2b-256 d2bf58d7573726f43896973ac6d16a62aef3cc40df720000930d917042561743

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