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);