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

Uploaded Python 3

File details

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

File metadata

  • Download URL: py_sse-0.10.0a1.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.0a1.tar.gz
Algorithm Hash digest
SHA256 8c7acb61aaf6a0a33b4cbbff8dfe53efc8904bacedbba2ce7f761de3776c9c8d
MD5 9ecf144ef56763a12a3833f5af69aa3c
BLAKE2b-256 d27d77aed88be79394013be184b32fc61e7c6277f1953c6188bae95a8626a684

See more details on using hashes here.

File details

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

File metadata

  • Download URL: py_sse-0.10.0a1-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.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 fb4d5e7df7b50516ab0f2ff33b7f07d88ce50e56888fb59a9e2892f32b329085
MD5 a0c1618d845843a263af5bf43abe1aea
BLAKE2b-256 0f3bfe627c33255415790f41fe3fd890e8701dd4cd7c015cc2b3f4b86f30ba07

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