GitHub

Example: Blog

SQL database, server functions, route parameters, 404s and validation errors.

  • 84 lines of Python
  • 2 pages
  • 1 server function
  • 734 B page JS (gzip)
This example needs its server (database, sessions or server functions). Run it locally:
Terminal
pyweb dev examples/blog/app.pyweb

#Source

examples/blog/app.pyweb
"""Blog: SQL database, server functions, server-rendered pages, route params."""

import os

from pyweb import App, NotFound, RPCError, component, server
from pyweb.db import connect

app = App(title="PyWeb Blog", stylesheets=["/static/app.css"])

db = connect(os.environ.get("DATABASE_URL", "sqlite:///blog.db"))
db.execute(
    "create table if not exists posts ("
    " id integer primary key autoincrement,"
    " title text not null,"
    " body text not null)"
)


def all_posts():
    return db.execute("select id, title, body from posts order by id desc").dicts()


@server
def publish(title: str, body: str) -> list:
    title, body = title.strip(), body.strip()
    if not title or not body:
        raise RPCError("validation_error", "A post needs a title and a body.")
    db.execute("insert into posts (title, body) values (?, ?)", (title, body))
    return all_posts()


@component
def PostCard(post):
    <article>
        <h2><a href={"/posts/" + str(post["id"])}>{post["title"]}</a></h2>
        <p class="muted">{post["body"][:140]}</p>
    </article>


@app.page("/", title="PyWeb Blog")
def Home():
    posts = all_posts()
    title = ""
    body = ""
    error = ""

    def submit():
        try:
            posts = publish(title, body)
            title = ""
            body = ""
            error = ""
        except RPCError as e:
            error = str(e)

    <main>
        <h1>Blog</h1>
        <form onsubmit={submit}>
            <div style={{"flex": "1"}}>
                <input id="title" bind={title} placeholder="Title" />
                <textarea id="body" bind={body} placeholder="Write something…"></textarea>
                <p class="error" id="error">{error}</p>
                <button id="publish" class="primary">Publish</button>
            </div>
        </form>
        for post in posts:
            <PostCard post={post} />
        if not posts:
            <p class="empty">No posts yet.</p>
    </main>


@app.page("/posts/{post_id}")
def Post(post_id: int):
    rows = db.execute("select title, body from posts where id = ?", (post_id,)).dicts()
    if not rows:
        raise NotFound()
    post = rows[0]

    <main>
        <p><a href="/">← All posts</a></p>
        <h1>{post["title"]}</h1>
        <p>{post["body"]}</p>
    </main>

#What runs where

Output of pyweb inspect: every page variable, handler and server function, where it runs and why.

Output
page Home  route=/
  browser  posts          reactive state: assigned in submit(); initial value computed on the server and sent
  browser  title          reactive state: bound to an input (line 60); literal initial value
  browser  body           reactive state: bound to an input (line 61); literal initial value
  browser  error          reactive state: assigned in submit(); literal initial value
  browser  submit         event handler (compiled to JavaScript)
page Post  route=/posts/{post_id}
  server   rows           computed per request; never read by browser code, never sent
  server   post           computed per request on the server; value sent because browser code reads it
rpc POST /__pyweb/rpc/publish  (title: str, body: str) -> list

#Generated JavaScript

The browser modules the compiler wrote (before minification). They import the shared runtime.

JavaScript
// Home.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
function PostCard($p) {
  const post = $p["post"] || (() => null);
  return [$h("article", null, () => [
      $h("h2", null, () => [$h("a", {"href": () => $py.add("/posts/", $py.str($py.at(post(), "id")))}, () => [$dyn(() => $py.at(post(), "title"))])]),
      $h("p", {"class": "muted"}, () => [$dyn(() => $py.slice($py.at(post(), "body"), null, 140, null))])
    ])];
}
function Home($s) {
  const posts = $signal("posts" in $s ? $s["posts"] : null);
  const title = $signal("title" in $s ? $s["title"] : "");
  const body = $signal("body" in $s ? $s["body"] : "");
  const error = $signal("error" in $s ? $s["error"] : "");
  async function submit() {
    let e;
    try {
      posts((await $rpc("publish", {"title": title(), "body": body()})));
      title("");
      body("");
      error("");
    } catch ($err) {
      if ($py.exc($err, ["RPCError"])) {
        e = $err;
        error($py.str(e));
      } else { throw $err; }
    }
  }
  return [$h("main", null, () => [
      $h("h1", null, () => [$t("Blog")]),
      $h("form", {"onsubmit": submit}, () => [$h("div", {"style": {"flex": "1"}}, () => [
          $h("input", {"id": "title", "$bind": title, "placeholder": "Title"}),
          $h("textarea", {"id": "body", "$bind": body, "placeholder": "Write something\u2026"}),
          $h("p", {"class": "error", "id": "error"}, () => [$dyn(() => error())]),
          $h("button", {"id": "publish", "class": "primary"}, () => [$t("Publish")])
        ])]),
      $list(() => posts(), (post) => [PostCard({"post": () => post})]),
      $when(() => !$py.truth(posts()), () => [$h("p", {"class": "empty"}, () => [$t("No posts yet.")])])
    ])];
}
$mount("Home", Home);
Edit this page on GitHub