Skip to content

Commit 41f30f3

Browse files
committed
adv_app.py: Use 'db-namespace' style approach
1 parent ddfe536 commit 41f30f3

File tree

1 file changed

+26
-23
lines changed

1 file changed

+26
-23
lines changed

examples/adv_app.py

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,22 @@
2323

2424
from hmac import compare_digest
2525

26-
# You can use any database you want; it'll be easier if you pick a lib that supports the MiniDataAPI spec.
27-
# Here we are using SQLite, with the FastLite library, which supports the MiniDataAPI spec.
26+
# We define database tables using dataclasses. These are simple classes with type annotations.
27+
# The `dataclass` decorator automatically generates a constructor, and other methods like `__repr__`, `__eq__`, etc.
28+
@dataclass
29+
class User: name:str; pwd:str
30+
@dataclass
31+
class Todo: id:int; title:str; done:bool; name:str; details:str; priority:int
32+
33+
#We use the `database` function from fastlist to create a sqlite database connection. If the data directory and
34+
# utodos.db file don't exist, they will be created automatically.
2835
db = database('data/utodos.db')
29-
# The `t` attribute is the table collection. The `todos` and `users` tables are not created if they don't exist.
30-
# Instead, you can use the `create` method to create them if needed.
31-
todos,users = db.t.todos,db.t.users
32-
if todos not in db.t:
33-
# You can pass a dict, or kwargs, to most MiniDataAPI methods.
34-
users.create(dict(name=str, pwd=str), pk='name')
35-
todos.create(id=int, title=str, done=bool, name=str, details=str, priority=int, pk='id')
36-
# Although you can just use dicts, it can be helpful to have types for your DB objects.
37-
# The `dataclass` method creates that type, and stores it in the object, so it will use it for any returned items.
38-
Todo,User = todos.dataclass(),users.dataclass()
36+
# The `create` method creates a table in the database, and returns a table object. We attach this to the `db` object
37+
# like a namespace so there are no collisions between the database objects and various functions and variables we'll
38+
# use in this app.
39+
# The `pk` argument specifies the primary key for the table. The `foreign_keys` argument specifies any foreign keys.
40+
db.users = db.create(User, pk='name')
41+
db.todos = db.create(Todo, pk='id', foreign_keys=[('name', 'user')])
3942

4043
# Any Starlette response class can be returned by a FastHTML route handler.
4144
# In that case, FastHTML won't change it at all.
@@ -54,7 +57,7 @@ def before(req, sess):
5457
if not auth: return login_redir
5558
# `xtra` is part of the MiniDataAPI spec. It adds a filter to queries and DDL statements,
5659
# to ensure that the user can only see/edit their own todos.
57-
todos.xtra(name=auth)
60+
db.todos.xtra(name=auth)
5861

5962
markdown_js = """
6063
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
@@ -133,10 +136,10 @@ def post(login:Login, sess):
133136
if not login.name or not login.pwd: return login_redir
134137
# Indexing into a MiniDataAPI table queries by primary key, which is `name` here.
135138
# It returns a dataclass object, if `dataclass()` has been called at some point, or a dict otherwise.
136-
try: u = users[login.name]
139+
try: u = db.users[login.name]
137140
# If the primary key does not exist, the method raises a `NotFoundError`.
138141
# Here we use this to just generate a user -- in practice you'd probably to redirect to a signup page.
139-
except NotFoundError: u = users.insert(login)
142+
except NotFoundError: u = db.users.insert(login)
140143
# This compares the passwords using a constant time string comparison
141144
# https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python
142145
if not compare_digest(u.pwd.encode("utf-8"), login.pwd.encode("utf-8")): return login_redir
@@ -202,7 +205,7 @@ def get(auth):
202205
# The reason we put the todo list inside a form is so that we can use the 'sortable' js library to reorder them.
203206
# That library calls the js `end` event when dragging is complete, so our trigger here causes our `/reorder`
204207
# handler to be called.
205-
frm = Form(*todos(order_by='priority'),
208+
frm = Form(*db.todos(order_by='priority'),
206209
id='todo-list', cls='sortable', hx_post="/reorder", hx_trigger="end")
207210
# We create an empty 'current-todo' Div at the bottom of our page, as a target for the details and editing views.
208211
card = Card(Ul(frm), header=add, footer=Div(id='current-todo'))
@@ -224,13 +227,13 @@ def get(auth):
224227
# the parameter is a list of ints.
225228
@rt("/reorder")
226229
def post(id:list[int]):
227-
for i,id_ in enumerate(id): todos.update({'priority':i}, id_)
230+
for i,id_ in enumerate(id): db.todos.update({'priority':i}, id_)
228231
# HTMX by default replaces the inner HTML of the calling element, which in this case is the todo list form.
229232
# Therefore, we return the list of todos, now in the correct order, which will be auto-converted to FT for us.
230233
# In this case, it's not strictly necessary, because sortable.js has already reorder the DOM elements.
231234
# However, by returning the updated data, we can be assured that there aren't sync issues between the DOM
232235
# and the server.
233-
return tuple(todos(order_by='priority'))
236+
return tuple(db.todos(order_by='priority'))
234237

235238
# Refactoring components in FastHTML is as simple as creating Python functions.
236239
# The `clr_details` function creates a Div with specific HTMX attributes.
@@ -242,7 +245,7 @@ def clr_details(): return Div(hx_swap_oob='innerHTML', id='current-todo')
242245
@rt("/todos/{id}")
243246
def delete(id:int):
244247
# The `delete` method is part of the MiniDataAPI spec, removing the item with the given primary key.
245-
todos.delete(id)
248+
db.todos.delete(id)
246249
# Returning `clr_details()` ensures the details view is cleared after deletion,
247250
# leveraging HTMX's out-of-band swap feature.
248251
# Note that we are not returning *any* FT component that doesn't have an "OOB" swap, so the target element
@@ -260,14 +263,14 @@ def get(id:int):
260263
# `fill_form` populates the form with existing todo data, and returns the result.
261264
# Indexing into a table (`todos`) queries by primary key, which is `id` here. It also includes
262265
# `xtra`, so this will only return the id if it belongs to the current user.
263-
return fill_form(res, todos[id])
266+
return fill_form(res, db.todos[id])
264267

265268
@rt("/")
266269
def put(todo: Todo):
267270
# `update` is part of the MiniDataAPI spec.
268271
# Note that the updated todo is returned. By returning the updated todo, we can update the list directly.
269272
# Because we return a tuple with `clr_details()`, the details view is also cleared.
270-
return todos.update(todo), clr_details()
273+
return db.todos.update(todo), clr_details()
271274

272275
@rt("/")
273276
def post(todo:Todo):
@@ -276,11 +279,11 @@ def post(todo:Todo):
276279
new_inp = Input(id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true')
277280
# `insert` returns the inserted todo, which is appended to the start of the list, because we used
278281
# `hx_swap='afterbegin'` when creating the todo list form.
279-
return todos.insert(todo), new_inp
282+
return db.todos.insert(todo), new_inp
280283

281284
@rt("/todos/{id}")
282285
def get(id:int):
283-
todo = todos[id]
286+
todo = db.todos[id]
284287
# `hx_swap` determines how the update should occur. We use "outerHTML" to replace the entire todo `Li` element.
285288
btn = Button('delete', hx_delete=f'/todos/{todo.id}',
286289
target_id=f'todo-{todo.id}', hx_swap="outerHTML")

0 commit comments

Comments
 (0)