Fasthtml Docs
Fasthtml Docs
ml
FastHTML
The fastest, most powerful way to create an HTML app
Welcome to the official FastHTML documentation.
FastHTML is a new next-generation web framework for fast, scalable web applications
with minimal, compact code. It’s designed to be:
Powerful and expressive enough to build the most advanced, interactive web apps you
can imagine.
Fast and lightweight, so you can write less code and get more done.
Easy to learn and use, with a simple, intuitive syntax that makes it easy to build
complex apps quickly.
FastHTML apps are just Python code, so you can use FastHTML with the full power of
the Python language and ecosystem. FastHTML’s functionality maps 1:1 directly to
HTML and HTTP, but allows them to be encapsulated using good software engineering
practices—so you’ll need to understand these foundations to use this library fully.
To understand how and why this works, please read this first: about.fastht.ml.
Installation
Since fasthtml is a Python library, you can install it with:
In the near future, we hope to add component libraries that can likewise be
installed via pip.
Usage
For a minimal app, create a file “main.py” as follows:
main.py
from fasthtml.common import *
app,rt = fast_app()
@rt('/')
def get(): return Div(P('Hello World!'), hx_get="/change")
serve()
Running the app with python main.py prints out a link to your running app:
http://localhost:5001. Visit that link in your browser and you should see a page
with the text “Hello World!”. Congratulations, you’ve just created your first
FastHTML app!
Adding interactivity is surprisingly easy, thanks to HTMX. Modify the file to add
this function:
main.py
@rt('/change')
def get(): return P('Nice to be here!')
You now have a page with a clickable element that changes the text when clicked.
When clicking on this link, the server will respond with an “HTML partial”—that is,
just a snippet of HTML which will be inserted into the existing page. In this case,
the returned element will replace the original P element (since that’s the default
behavior of HTMX) with the new version returned by the second route.
This “hypermedia-based” approach to web development is a powerful way to build web
applications.
/llms-ctx.txt
This example is in a format based on recommendations from Anthropic for use with
Claude Projects. This works so well that we’ve actually found that Claude can
provide even better information than our own documentation! For instance, read
through this annotated Claude chat for some great getting-started information,
entirely generated from a project using the above text file as context.
If you use Cursor, type @doc then choose “Add new doc”, and use the /llms-ctx.txt
link above. The context file is auto-generated from our llms.txt (our proposed
standard for providing AI-friendly information)—you can generate alternative
versions suitable for other models as needed.
Next Steps
Start with the official sources to learn more about FastHTML:
The capabilities of FastHTML are vast and growing, and not all the features and
patterns have been documented yet. Be prepared to invest time into studying and
modifying source code, such as the main FastHTML repo’s notebooks and the official
FastHTML examples repo:
FastHTML Gallery: Learn from minimal examples of components (ie chat bubbles,
click-to-edit, infinite scroll, etc)
Creating Custom FastHTML Tags for Markdown Rendering by Isaac Flath
How to Build a Simple Login System in FastHTML by Marius Vach
Your tutorial here!
Finally, join the FastHTML community to ask questions, share your work, and learn
from others:
Discord
Other languages and related projects
If you’re not a Python user, or are keen to try out a new language, we’ll list here
other projects that have a similar approach to FastHTML. (Please reach out if you
know of any other projects that you’d like to see added.)
htmgo (Go): “htmgo is a lightweight pure go way to build interactive websites / web
applications using go & htmx. By combining the speed & simplicity of go +
hypermedia attributes (htmx) to add interactivity to websites, all conveniently
wrapped in pure go, you can build simple, fast, interactive websites without
touching javascript. All compiled to a single deployable binary”
If you’re just interested in functional HTML components, rather than a full HTMX
server solution, consider:
---
https://docs.fastht.ml/tutorials/by_example.html
FastHTML By Example
An introduction to FastHTML from the ground up, with four complete examples
This tutorial provides an alternate introduction to FastHTML by building out
example applications. We also illustrate how to use FastHTML foundations to create
custom web apps. Finally, this document serves as minimal context for a LLM to turn
it into a FastHTML assistant.
FastHTML Basics
FastHTML is just Python. You can install it with pip install python-fasthtml.
Extensions/components built for it can likewise be distributed via PyPI or as
simple Python files.
The core usage of FastHTML is to define routes, and then to define what to do at
each route. This is similar to the FastAPI web framework (in fact we implemented
much of the functionality to match the FastAPI usage examples), but where FastAPI
focuses on returning JSON data to build APIs, FastHTML focuses on returning HTML
data.
app = FastHTML()
@app.get("/")
def home():
return "<h1>Hello, World</h1>"
serve()
To run this app, place it in a file, say app.py, and then run it with python
app.py.
Constructing HTML
Notice we wrote some HTML in the previous example. We don’t want to do that! Some
web frameworks require that you learn HTML, CSS, JavaScript AND some templating
language AND python. We want to do as much as possible with just one language.
Fortunately, the Python module fastcore.xml has all we need for constructing HTML
from Python, and FastHTML includes all the tags you need to get started. For
example:
<!doctype html></!doctype>
<html>
<head>
<title>Some page</title>
</head>
<body>
<div class="myclass">
Some text,
<a href="https://example.com">A link</a>
<img src="https://placehold.co/200">
</div>
</body>
</html>
show(page)
FastHTML is smart enough to know about fastcore.xml, and so you don’t need to use
the to_xml function to convert your FT objects to HTML. You can just return them as
you would any other Python object. For example, if we modify our previous example
to use fastcore.xml, we can return an FT object directly:
@app.get("/")
def home():
page = Html(
Head(Title('Some page')),
Body(Div('Some text, ', A('A link', href='https://example.com'),
Img(src="https://placehold.co/200"), cls='myclass')))
return page
serve()
For debugging, you can right-click on the rendered HTML in the browser and select
“Inspect” to see the underlying HTML that was generated. There you’ll also find the
‘network’ tab, which shows you the requests that were made to render the page.
Refresh and look for the request to 127.0.0.1 - and you’ll see it’s just a GET
request to /, and the response body is the HTML you just returned.
Live Reloading
You can also enable live reloading so you don’t have to manually refresh your
browser to view updates.
<html>
<head><title>Some page</title>
</head>
<body><div class="myclass">
Some text,
<a href="https://example.com">A link</a>
<img src="https://placehold.co/200">
</div>
</body>
</html>
FastHTML wraps things in an Html tag if you don’t do it yourself (unless the
request comes from htmx, in which case you get the element directly). See FT
objects and HTML for more on creating custom components or adding HTML rendering to
existing Python objects. To give the page a non-default title, return a Title
before your main content:
app = FastHTML()
@app.get("/")
def home():
return Title("Page Demo"), Div(H1('Hello, World'), P('Some text'), P('Some more
text'))
client = TestClient(app)
print(client.get("/").text)
<!doctype html></!doctype>
<html>
<head>
<title>Page Demo</title>
<meta charset="utf-8"></meta>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-
fit=cover"></meta>
<script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script>
<script
src="https://cdn.jsdelivr.net/gh/answerdotai/[email protected]/surreal.js"></script>
<script
src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
</head>
<body>
<div>
<h1>Hello, World</h1>
<p>Some text</p>
<p>Some more text</p>
</div>
</body>
</html>
We’ll use this pattern often in the examples to follow.
Defining Routes
The HTTP protocol defines a number of methods (‘verbs’) to send requests to a
server. The most common are GET, POST, PUT, DELETE, and HEAD. We saw ‘GET’ in
action before - when you navigate to a URL, you’re making a GET request to that
URL. We can do different things on a route for different HTTP methods. For example:
@app.route("/", methods='get')
def home():
return H1('Hello, World')
This says that when someone navigates to the root URL “/” (i.e. sends a GET
request), they will see the big “Hello, World” heading. When someone submits a POST
or PUT request to the same URL, the server should return the string “got a post or
put request”.
There are a few other ways you can specify the route+method - FastHTML
has .get, .post, etc. as shorthand for route(..., methods=['get']), etc.
@app.get("/")
def my_function():
return "Hello World from a GET request"
Or you can use the @rt decorator without a method but specify the method with the
name of the function. For example:
rt = app.route
@rt("/")
def post():
return "Hello World from a POST request"
client.post("/").text
'Hello World from a POST request'
You’re welcome to pick whichever style you prefer. Using routes lets you show
different content on different pages - ‘/home’, ‘/about’ and so on. You can also
respond differently to different kinds of requests to the same route, as shown
above. You can also pass data via the route:
@app.get
@rt
@app.get("/greet/{nm}")
def greet(nm:str):
return f"Good day to you, {nm}!"
client.get("/greet/Dave").text
Styling Basics
Plain HTML probably isn’t quite what you imagine when you visualize your beautiful
web app. CSS is the go-to language for styling HTML. But again, we don’t want to
learn extra languages unless we absolutely have to! Fortunately, there are ways to
get much more visually appealing sites by relying on the hard work of others, using
existing CSS libraries. One of our favourites is PicoCSS. A common way to add CSS
files to web pages is to use a <link> tag inside your HTML header, like this:
<header>
...
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
</header>
For convenience, FastHTML already defines a Pico component for you with picolink:
print(to_xml(picolink))
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
Since we typically want CSS styling on all pages of our app, FastHTML lets you
define a shared HTML header with the hdrs argument as shown below:
@app.route("/")
def get():
return (Title("Hello World"),
3 Main(H1('Hello, World'), cls="container"))
1
Custom styling to override the pico defaults
2
Define shared headers for all pages
3
As per the pico docs, we put all of our content inside a <main> tag with a class of
container:
Returning Tuples
We’re returning a tuple here (a title and the main page). Returning a tuple, list,
FT object, or an object with a __ft__ method tells FastHTML to turn the main body
into a full HTML page that includes the headers (including the pico link and our
custom css) which we passed in. This only occurs if the request isn’t from HTMX
(for HTMX requests we need only return the rendered components).
You can check out the Pico examples page to see how different elements will look.
If everything is working, the page should now render nice text with our custom
font, and it should respect the user’s light/dark mode preferences too.
If you want to override the default styles or add more custom CSS, you can do so by
adding a <style> tag to the headers as shown above. So you are allowed to write CSS
to your heart’s content - we just want to make sure you don’t necessarily have to!
Later on we’ll see examples using other component libraries and tailwind css to do
more fancy styling things, along with tips to get an LLM to write all those fiddly
bits so you don’t have to.
app = FastHTML()
messages = ["This is a message, which will get rendered as a paragraph"]
@app.get("/")
def home():
return Main(H1('Messages'),
*[P(msg) for msg in messages],
A("Link to Page 2 (to add messages)", href="/page2"))
@app.get("/page2")
def page2():
return Main(P("Add a message with the form below:"),
Form(Input(type="text", name="data"),
Button("Submit"),
action="/", method="post"))
@app.post("/")
def add_message(data:str):
messages.append(data)
return home()
We re-render the entire homepage to show the newly added message. This is fine, but
modern web apps often don’t re-render the entire page, they just update a part of
the page. In fact even very complicated applications are often implemented as
‘Single Page Apps’ (SPAs). This is where HTMX comes in.
HTMX
HTMX addresses some key limitations of HTML. In vanilla HTML, links can trigger a
GET request to show a new page, and forms can send requests containing data to the
server. A lot of ‘Web 1.0’ design revolved around ways to use these to do
everything we wanted. But why should only some elements be allowed to trigger
requests? And why should we refresh the entire page with the result each time one
does? HTMX extends HTML to allow us to trigger requests from any element on all
kinds of events, and to update a part of the page without refreshing the entire
page. It’s a powerful tool for building modern web apps.
It does this by adding attributes to HTML tags to make them do things. For example,
here’s a page with a counter and a button that increments it:
app = FastHTML()
count = 0
@app.get("/")
def home():
return Title("Count Demo"), Main(
H1("Count Demo"),
P(f"Count is set to {count}", id="count"),
Button("Increment", hx_post="/increment", hx_target="#count",
hx_swap="innerHTML")
)
@app.post("/increment")
def increment():
print("incrementing")
global count
count += 1
return f"Count is set to {count}"
This pattern of having elements trigger requests that modify or replace other
elements is a key part of the HTMX philosophy. It takes a little getting used to,
but once mastered it is extremely powerful.
image.png
We’ve made a number of variants of this app - so in addition to the version shown
in the video you can browse this series of examples with increasing complexity, the
heavily-commented “idiomatic” version here, and the example linked from the
FastHTML homepage.
# Main page
@app.get("/")
def get():
inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list',
hx_swap="afterbegin")
gen_list = Div(id='gen-list')
return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add,
gen_list, cls='container')
Submitting the form will trigger a POST request to /, so next we need to generate
an image and add it to the list. One problem: generating images is slow! We’ll
start the generation in a separate thread, but this now surfaces a different
problem: we want to update the UI right away, but our image will only be ready a
few seconds later. This is a common pattern - think about how often you see a
loading spinner online. We need a way to return a temporary bit of UI which will
eventually be replaced by the final image. Here’s how we might do this:
def generation_preview(id):
if os.path.exists(f"gens/{id}.png"):
return Div(Img(src=f"/gens/{id}.png"), id=f'gen-{id}')
else:
return Div("Generating...", id=f'gen-{id}',
hx_post=f"/generations/{id}",
hx_trigger='every 1s', hx_swap='outerHTML')
@app.post("/generations/{id}")
def get(id:int): return generation_preview(id)
@app.post("/")
def post(prompt:str):
id = len(generations)
generate_and_save(prompt, id)
generations.append(prompt)
clear_input = Input(id="new-prompt", name="prompt", placeholder="Enter a
prompt", hx_swap_oob='true')
return generation_preview(id), clear_input
@threaded
def generate_and_save(prompt, id): ...
The form sends the prompt to the / route, which starts the generation in a separate
thread then returns two things:
A generation preview element that will be added to the top of the gen-list div
(since that is the target_id of the form which triggered the request)
An input field that will replace the form’s input field (that has the same id),
using the hx_swap_oob=‘true’ trick. This clears the prompt field so the user can
type another prompt.
The generation preview first returns a temporary “Generating…” message, which polls
the /generations/{id} route every second. This is done by setting hx_post to the
route and hx_trigger to ‘every 1s’. The /generations/{id} route returns the preview
element every second until the image is ready, at which point it returns the final
image. Since the final image replaces the temporary one (hx_swap=‘outerHTML’), the
polling stops running and the generation preview is now complete.
This works nicely - the user can submit several prompts without having to wait for
the first one to generate, and as the images become available they are added to the
list. You can see the full code of this version here.
image.png
Step one was looking around for existing components. The Pico CSS library we’ve
been using has a rudimentary grid but recommends using an alternative layout
system. One of the options listed was Flexbox.
To use Flexbox you create a “row” with one or more elements. You can specify how
wide things should be with a specific syntax in the class name. For example, col-
xs-12 means a box that will take up 12 columns (out of 12 total) of the row on
extra small screens, col-sm-6 means a column that will take up 6 columns of the row
on small screens, and so on. So if you want four columns on large screens you would
use col-lg-3 for each item (i.e. each item is using 3 columns out of 12).
<div class="row">
<div class="col-xs-12">
<div class="box">This takes up the full width</div>
</div>
</div>
This was non-intuitive to me. Thankfully ChatGPT et al know web stuff quite well,
and we can also experiment in a notebook to test things out:
grid = Html(
Link(rel="stylesheet",
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
, type="text/css"),
Div(
Div(Div("This takes up the full width", cls="box", style="background-color:
#800000;"), cls="col-xs-12"),
Div(Div("This takes up half", cls="box", style="background-color:
#008000;"), cls="col-xs-6"),
Div(Div("This takes up half", cls="box", style="background-color:
#0000B0;"), cls="col-xs-6"),
cls="row", style="color: #fff;"
)
)
show(grid)
Translating this into our app, we have a new homepage with a div (class="row") to
store the generated images / previews, and a generation_preview function that
returns boxes with the appropriate classes and styles to make them appear in the
grid. I chose a layout with different numbers of columns for different screen
sizes, but you could also just specify the col-xs class if you wanted the same
layout on all devices.
gridlink = Link(rel="stylesheet",
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
, type="text/css")
app = FastHTML(hdrs=(picolink, gridlink))
# Main page
@app.get("/")
def get():
inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list',
hx_swap="afterbegin")
gen_containers = [generation_preview(g) for g in gens(limit=10)] # Start with
last 10
gen_list = Div(*gen_containers[::-1], id='gen-list', cls="row") # flexbox
container: class = row
return Title('Image Generation Demo'), Main(H1('Magic Image Generation'), add,
gen_list, cls='container')
You can see the final result in main.py in the image_app_simple example directory,
along with info on deploying it (tl;dr don’t!). We’ve also deployed a version that
only shows your generations (tied to browser session) and has a credit system to
save our bank accounts. You can access that here. Now for the next question: how do
we keep track of different users?
Again, with Sessions
At the moment everyone sees all images! How do we keep some sort of unique
identifier tied to a user? Before going all the way to setting up users, login
pages etc., let’s look at a way to at least limit generations to the user’s
session. You could do this manually with cookies. For convenience and security,
fasthtml (via Starlette) has a special mechanism for storing small amounts of data
in the user’s browser via the session argument to your route. This acts like a
dictionary and you can set and get values from it. For example, here we look for a
session_id key, and if it doesn’t exist we generate a new one:
@app.get("/")
def get(session):
if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())
return H1(f"Session ID: {session['session_id']}")
Refresh the page a few times - you’ll notice that the session ID remains the same.
If you clear your browsing data, you’ll get a new session ID. And if you load the
page in a different browser (but not a different tab), you’ll get a new session ID.
This will persist within the current browser, letting us use it as a key for our
generations. As a bonus, someone can’t spoof this session id by passing it in
another way (for example, sending a query parameter). Behind the scenes, the data
is stored in a browser cookie but it is signed with a secret key that stops the
user or anyone nefarious from being able to tamper with it. The cookie is decoded
back into a dictionary by something called a middleware function, which we won’t
cover here. All you need to know is that we can use this to store bits of state in
the user’s browser.
In the image app example, we can add a session_id column to our database, and
modify our homepage like so:
@app.get("/")
def get(session):
if 'session_id' not in session: session['session_id'] = str(uuid.uuid4())
inp = Input(id="new-prompt", name="prompt", placeholder="Enter a prompt")
add = Form(Group(inp, Button("Generate")), hx_post="/", target_id='gen-list',
hx_swap="afterbegin")
gen_containers = [generation_preview(g) for g in gens(limit=10,
where=f"session_id == '{session['session_id']}'")]
...
So we check if the session id exists in the session, add one if not, and then limit
the generations shown to only those tied to this session id. We filter the database
with a where clause - see [TODO link Jeremy’s example for a more reliable way to do
this]. The only other change we need to make is to store the session id in the
database when a generation is made. You can check out this version here. You could
instead write this app without relying on a database at all - simply storing the
filenames of the generated images in the session, for example. But this more
general approach of linking some kind of unique session identifier to users or data
in our tables is a useful general pattern for more complex examples.
A way to create a Stripe checkout session and redirect the user to the session URL
‘Success’ and ‘Cancel’ routes to handle the result of the checkout
A route that listens for a webhook from Stripe to update the number of credits when
a payment is made.
In a typical application you’ll want to keep track of which users make payments,
catch other kinds of stripe events and so on. This example is more a ‘this is
possible, do your own research’ than ‘this is how you do it’. But hopefully it does
illustrate the key idea: there is no magic here. Stripe (and many other
technologies) relies on sending users to different routes and shuttling data back
and forth in requests. And we know how to do that!
request (or any prefix like req): gets the raw Starlette Request object
session (or any prefix like sess): gets the session object
auth
htmx
app
In this section let’s quickly look at some of these in action.
app = FastHTML()
cli = TestClient(app)
@app.get('/user/{nm}')
def _(nm:str): return f"Good day to you, {nm}!"
cli.get('/user/jph').text
reg_re_param("imgext", "ico|gif|jpg|jpeg|webm")
@app.get(r'/static/{path:path}/{fn}.{ext:imgext}')
def get_img(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
cli.get('/static/foo/jph.ico').text
@app.get("/models/{nm}")
def model(nm:ModelName): return nm
print(cli.get('/models/alexnet').text)
alexnet
Casting to a Path:
@app.get("/files/{path}")
def txt(path: Path): return path.with_suffix('.txt')
print(cli.get('/files/foo').text)
foo.txt
An integer with a default value:
@app.get("/items/")
def read_item(idx: int = 0): return fake_db[idx]
print(cli.get('/items/?idx=1').text)
{"name":"Bar"}
# Equivalent to `/items/?idx=0`.
print(cli.get('/items/').text)
{"name":"Foo"}
Boolean values (takes anything “truthy” or “falsy”):
@app.get("/booly/")
def booly(coming:bool=True): return 'Coming' if coming else 'Not coming'
print(cli.get('/booly/?coming=true').text)
Coming
print(cli.get('/booly/?coming=no').text)
Not coming
Getting dates:
@app.get("/datie/")
def datie(d:parsed_date): return d
2024-05-17 14:00:00
Matching a dataclass:
@app.route("/bodie/{nm}")
def post(nm:str, data:Bodie):
res = asdict(data)
res['nm'] = nm
return res
'{"a":1,"b":"foo","nm":"me"}'
Cookies
Cookies can be set via a Starlette Response object, and can be read back by
specifying the name:
@app.get("/setcookie")
def setc(req):
now = datetime.now()
res = Response(f'Set to {now}')
res.set_cookie('now', str(now))
return res
cli.get('/setcookie').text
cli.get('/getcookie').text
@app.get("/ua")
async def ua(user_agent:str): return user_agent
cli.get('/ua', headers={'User-Agent':'FastHTML'}).text
'FastHTML'
@app.get("/hxtest")
def hxtest(htmx): return htmx.request
cli.get('/hxtest', headers={'HX-Request':'1'}).text
'1'
Starlette Requests
If you add an argument called request(or any prefix of that, for example req) it
will be populated with the Starlette Request object. This is useful if you want to
do your own processing manually. For example, although FastHTML will parse forms
for you, you could instead get form data like so:
@app.get("/form")
async def form(request:Request):
form_data = await request.form()
a = form_data.get('a')
See the Starlette docs for more information on the Request object.
Starlette Responses
You can return a Starlette Response object from a route to control the response.
For example:
@app.get("/redirect")
def redirect():
return RedirectResponse(url="/")
We used this to set cookies in the previous example. See the Starlette docs for
more information on the Response object.
Static Files
We often want to serve static files like images. This is easily done! For common
file types (images, CSS etc) we can create a route that returns a Starlette
FileResponse like so:
You can customize it to suit your needs (for example, only serving files in a
certain directory). You’ll notice some variant of this route in all our complete
examples - even for apps with no static files the browser will typically request
a /favicon.ico file, for example, and as the astute among you will have noticed
this has sparked a bit of competition between Johno and Jeremy regarding which
country flag should serve as the default!
WebSockets
For certain applications such as multiplayer games, websockets can be a powerful
feature. Luckily HTMX and FastHTML has you covered! Simply specify that you wish to
include the websocket header extension from HTMX:
app = FastHTML(exts='ws')
rt = app.route
With that, you are now able to specify the different websocket specific HTMX
goodies. For example, say we have a website we want to setup a websocket, you can
simply:
@rt('/')
async def get(request):
cts = Div(
Div(id='notifications'),
Form(mk_inp(), id='form', ws_send=True),
hx_ext='ws', ws_connect='/ws')
return Titled('Websocket Test', cts)
And this will setup a connection on the route /ws along with a form that will send
a message to the websocket whenever the form is submitted. Let’s go ahead and
handle this route:
@app.ws('/ws')
async def ws(msg:str, send):
await send(Div('Hello ' + msg, id="notifications"))
await sleep(2)
return Div('Goodbye ' + msg, id="notifications"), mk_inp()
One thing you might have noticed is a lack of target id for our websocket trigger
for swapping HTML content. This is because HTMX always swaps content with
websockets with Out of Band Swaps. Therefore, HTMX will look for the id in the
returned HTML content from the server for determining what to swap. To send stuff
to the client, you can either use the send parameter or simply return the content
or both!
Now, sometimes you might want to perform actions when a client connects or
disconnects such as add or remove a user from a player queue. To hook into these
events, you can pass your connection or disconnection function to the app.ws
decorator:
image.png
At first glance, DaisyUI’s chat component looks quite intimidating. The examples
look like this:
We can strip out some unnecessary bits and try to get the simplest possible example
working in a notebook first:
Now we can extend this to render multiple messages, with the message being on the
left (chat-start) or right (chat-end) depending on the role. While we’re at it, we
can also change the color (chat-bubble-primary) of the message and put them all in
a chat-box div:
messages = [
{"role":"user", "content":"Hello"},
{"role":"assistant", "content":"Hi, how can I assist you?"}
]
def ChatMessage(msg):
return Div(
Div(msg['role'], cls="chat-header"),
Div(msg['content'], cls=f"chat-bubble chat-bubble-{'primary' if msg['role']
== 'user' else 'secondary'}"),
cls=f"chat chat-{'end' if msg['role'] == 'user' else 'start'}")
chatbox = Div(*[ChatMessage(msg) for msg in messages], cls="chat-box",
id="chatlist")
Next, it was back to the ChatGPT to tweak the chat box so it wouldn’t grow as
messages were added. I asked:
To put it another way: none of the CSS classes in the following example were
written by a human, and what edits I did make were informed by advice from the AI
that made it relatively painless!
The actual chat functionality of the app is based on our claudette library. As with
the image example, we face a potential hiccup in that getting a response from an
LLM is slow. We need a way to have the user message added to the UI immediately,
and then have the response added once it’s available. We could do something similar
to the image generation example above, or use websockets. Check out the full
example for implementations of both, along with further details.
This would be a very dull game if we were to run it, since the initial state of
everything would remain dead. Therefore, we need a way of letting the user give an
initial state before starting the game. FastHTML to the rescue!
def Grid():
cells = []
for y, row in enumerate(game_state['grid']):
for x, cell in enumerate(row):
cell_class = 'alive' if cell else 'dead'
cell = Div(cls=f'cell {cell_class}', hx_put='/update', hx_vals={'x': x,
'y': y}, hx_swap='none', hx_target='#gol', hx_trigger='click')
cells.append(cell)
return Div(*cells, id='grid')
@rt('/update')
async def put(x: int, y: int):
grid[y][x] = 1 if grid[y][x] == 0 else 0
Above is a component for representing the game’s state that the user can interact
with and update on the server using cool HTMX features such as hx_vals for
determining which cell was clicked to make it dead or alive. Now, you probably
noticed that the HTTP request in this case is a PUT request, which does not return
anything and this means our client’s view of the grid world and the server’s game
state will immediately become out of sync :(. We could of course just return a new
Grid component with the updated state, but that would only work for a single
client, if we had more, they quickly get out of sync with each other and the
server. Now Websockets to the rescue!
Websockets are a way for the server to keep a persistent connection with clients
and send data to the client without explicitly being requested for information,
which is not possible with HTTP. Luckily FastHTML and HTMX work well with
Websockets. Simply state you wish to use websockets for your app and define a
websocket route:
...
app = FastHTML(hdrs=(picolink, gridlink, css, htmx_ws), exts='ws')
player_queue = []
async def update_players():
for i, player in enumerate(player_queue):
try: await player(Grid())
except: player_queue.pop(i)
async def on_connect(send): player_queue.append(send)
async def on_disconnect(send): await update_players()
@rt('/update')
async def put(x: int, y: int):
grid[y][x] = 1 if grid[y][x] == 0 else 0
await update_players()
...
Here we simply keep track of all the players that have connected or disconnected to
our site and when an update occurs, we send updates to all the players still
connected via websockets. Via HTMX, you are still simply exchanging HTML from the
server to the client and will swap in the content based on how you setup your
hx_swap attribute. There is only one difference, that being all swaps are OOB. You
can find more information on the HTMX websocket extension documentation page here.
You can find a full fledge hosted example of this app here.
For example, here’s one way we could make a custom class that can be rendered into
HTML:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __ft__(self):
return ['div', [f'{self.name} is {self.age} years old.'], {}]
p = Person('Jonathan', 28)
print(to_xml(Div(p, "more text", cls="container")))
<div class="container">
<div>Jonathan is 28 years old.</div>
more text
</div>
In the examples, you’ll see we often patch in __ft__ methods to existing classes to
control how they’re rendered. For example, if Person didn’t have a __ft__ method or
we wanted to override it, we could add a new one like this:
@patch
def __ft__(self:Person):
return Div("Person info:", Ul(Li("Name:",self.name), Li("Age:", self.age)))
show(p)
Person info:
Name: Jonathan
Age: 28
Some tags from fastcore.xml are overwritten by fasthtml.core and a few are further
extended by fasthtml.xtend using this method. Over time, we hope to see others
developing custom components too, giving us a larger and larger ecosystem of
reusable components.
For example, to use the marked.js library to render markdown in a div, including in
components added after the page has loaded via htmx, we do something like this:
The AI Pictionary example uses a larger chunk of custom JavaScript to handle the
drawing canvas. It’s a good example of the type of application where running code
on the client side makes the most sense, but still shows how you can integrate it
with FastHTML on the server side to add functionality (like the AI responses)
easily.
Adding styling with custom CSS and libraries such as tailwind is done the same way
we add custom JavaScript. The doodle example uses Doodle.CSS to style the page in a
quirky way.
Railway
Install the Railway CLI and sign up for an account.
Set up a folder with our app as main.py
In the folder, run railway login.
Use the fh_railway_deploy script to deploy our project:
fh_railway_deploy MY_APP_NAME
Replit
Fork this repl for a minimal example you can edit to your heart’s content. .replit
has been edited to add the right run command (run = ["uvicorn", "main:app", "--
reload"]) and to set up the ports correctly. FastHTML was installed with poetry add
python-fasthtml, you can add additional packages as needed in the same way. Running
the app in Replit will show you a webview, but you may need to open in a new tab
for all features (such as cookies) to work. When you’re ready, you can deploy your
app by clicking the ‘Deploy’ button. You pay for usage - for an app that is mostly
idle the cost is usually a few cents per month.
You can store secrets like API keys via the ‘Secrets’ tab in the Replit project
settings.
HuggingFace
Follow the instructions in this repository to deploy to HuggingFace spaces.
Where Next?
We’ve covered a lot of ground here! Hopefully this has given you plenty to work
with in building your own FastHTML apps. If you have any questions, feel free to
ask in the #fasthtml Discord channel (in the fastai Discord community). You can
look through the other examples in the fasthtml-example repository for more ideas,
and keep an eye on Jeremy’s YouTube channel where we’ll be releasing a number of
“dev chats” related to FastHTML in the near future.
---
https://docs.fastht.ml/tutorials/quickstart_for_web_devs.html
A Minimal Application
A minimal FastHTML application looks something like this:
main.py
1from fasthtml.common import *
2app, rt = fast_app()
3@rt("/")
4def get():
5 return Titled("FastHTML", P("Let's do this!"))
6serve()
1
We import what we need for rapid development! A carefully-curated set of FastHTML
functions and other Python objects is brought into our global namespace for
convenience.
2
We instantiate a FastHTML app with the fast_app() utility function. This provides a
number of really useful defaults that we’ll take advantage of later in the
tutorial.
3
We use the rt() decorator to tell FastHTML what to return when a user visits / in
their browser.
4
We connect this route to HTTP GET requests by defining a view function called
get().
5
A tree of Python function calls that return all the HTML required to write a
properly formed web page. You’ll soon see the power of this approach.
6
The serve() utility configures and runs FastHTML using a library called uvicorn.
Run the code:
python main.py
Note
While some linters and developers will complain about the wildcard import, it is by
design here and perfectly safe. FastHTML is very deliberate about the objects it
exports in fasthtml.common. If it bothers you, you can import the objects you need
individually, though it will make the code more verbose and less readable.
If you want to learn more about how FastHTML handles imports, we cover that here.
import json
from fasthtml.common import *
app, rt = fast_app(hdrs=(Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),))
data = json.dumps({
"data": [{"x": [1, 2, 3, 4],"type": "scatter"},
{"x": [1, 2, 3, 4],"y": [16, 5, 11, 9],"type": "scatter"}],
"title": "Plotly chart in FastHTML ",
"description": "This is a demo dashboard",
"type": "scatter"
})
@rt("/")
def get():
return Titled("Chart Demo", Div(id="myDiv"),
Script(f"var data = {data}; Plotly.newPlot('myDiv', data);"))
serve()
Debug Mode
When we can’t figure out a bug in FastHTML, we can run it in DEBUG mode. When an
error is thrown, the error screen is displayed in the browser. This error setting
should never be used in a deployed app.
1app, rt = fast_app(debug=True)
@rt("/")
def get():
2 1/0
return Titled("FastHTML Error!", P("Let's error!"))
serve()
1
debug=True sets debug mode on.
2
Python throws an error when it tries to divide an integer by zero.
Routing
FastHTML builds upon FastAPI’s friendly decorator pattern for specifying URLs, with
extra features:
main.py
from fasthtml.common import *
app, rt = fast_app()
1@rt("/")
def get():
return Titled("FastHTML", P("Let's do this!"))
2@rt("/hello")
def get():
return Titled("Hello, world!")
serve()
1
The “/” URL on line 5 is the home of a project. This would be accessed at
127.0.0.1:5001.
2
“/hello” URL on line 9 will be found by the project if the user visits
127.0.0.1:5001/hello.
Tip
It looks like get() is being defined twice, but that’s not the case. Each function
decorated with rt is totally separate, and is injected into the router. We’re not
calling them in the module’s namespace (locals()). Rather, we’re loading them into
the routing mechanism using the rt decorator.
You can do more! Read on to learn what we can do to make parts of the URL dynamic.
Variables in URLs
You can add variable sections to a URL by marking them with {variable_name}. Your
function then receives the {variable_name} as a keyword argument, but only if it is
the correct type. Here’s an example:
main.py
from fasthtml.common import *
app, rt = fast_app()
1@rt("/{name}/{age}")
2def get(name: str, age: int):
3 return Titled(f"Hello {name.title()}, age {age}")
serve()
1
We specify two variable names, name and age.
2
We define two function arguments named identically to the variables. You will note
that we specify the Python types to be passed.
3
We use these functions in our project.
Try it out by going to this address: 127.0.0.1:5001/uma/5. You should get a page
that says,
HTTP Methods
FastHTML matches function names to HTTP methods. So far the URL routes we’ve
defined have been for HTTP GET methods, the most common method for web pages.
Form submissions often are sent as HTTP POST. When dealing with more dynamic web
page designs, also known as Single Page Apps (SPA for short), the need can arise
for other methods such as HTTP PUT and HTTP DELETE. The way FastHTML handles this
is by changing the function name.
main.py
from fasthtml.common import *
app, rt = fast_app()
@rt("/")
1def get():
return Titled("HTTP GET", P("Handle GET"))
@rt("/")
2def post():
return Titled("HTTP POST", P("Handle POST"))
serve()
1
On line 6 because the get() function name is used, this will handle HTTP GETs going
to the / URI.
2
On line 10 because the post() function name is used, this will handle HTTP POSTs
going to the / URI.
CSS Files and Inline Styles
Here we modify default headers to demonstrate how to use the Sakura CSS
microframework instead of FastHTML’s default of Pico CSS.
main.py
from fasthtml.common import *
app, rt = fast_app(
1 pico=False,
hdrs=(
Link(rel='stylesheet', href='assets/normalize.min.css', type='text/css'),
2 Link(rel='stylesheet', href='assets/sakura.css', type='text/css'),
3 Style("p {color: red;}")
))
@app.get("/")
def home():
return Titled("FastHTML",
P("Let's do this!"),
)
serve()
1
By setting pico to False, FastHTML will not include pico.min.css.
2
This will generate an HTML <link> tag for sourcing the css for Sakura.
3
If you want an inline styles, the Style() function will put the result into the
HTML.
Other Static Media File Locations
As you saw, Script and Link are specific to the most common static media use cases
in web apps: including JavaScript, CSS, and images. But it also works with videos
and other static media files. The default behavior is to look for these files in
the root directory - typically we don’t do anything special to include them. We can
change the default directory that is looked in for files by adding the static_path
parameter to the fast_app function.
app, rt = fast_app(static_path='public')
FastHTML also allows us to define a route that uses FileResponse to serve the file
at a specified path. This is useful for serving images, videos, and other media
files from a different directory without having to change the paths of many files.
So if we move the directory containing the media files, we only need to change the
path in one place. In the example below, we call images from a directory called
public.
@rt("/{fname:path}.{ext:static}")
async def get(fname:str, ext:str):
return FileResponse(f'public/{fname}.{ext}')
Rendering Markdown
from fasthtml.common import *
app, rt = fast_app(hdrs=hdrs)
content = """
Here are some _markdown_ elements.
@rt('/')
def get(req):
return Titled("Markdown rendering example", Div(content,cls="marked"))
serve()
Code highlighting
Here’s how to highlight code without any markdown configuration.
app, rt = fast_app(hdrs=hdrs)
code_example = """
import datetime
import time
for i in range(10):
print(f"{datetime.datetime.now()}")
time.sleep(1)
"""
@rt('/')
def get(req):
return Titled("Markdown rendering example",
Div(
# The code example needs to be surrounded by
# Pre & Code elements
Pre(Code(code_example))
))
serve()
# usage example
Main(
hero("Hello World", "This is a hero statement")
)
<main> <div class="hero">
<h1>Hello World</h1>
<p>This is a hero statement</p>
</div>
</main>
# usage example
layout(
Ul(*[Li(o) for o in range(3)]),
P("Some content", cls="description"),
)
Dataclasses as ft components
While functions are easy to read, for more complex components some might find it
easier to use a dataclass.
@dataclass
class Hero:
title: str
statement: str
def __ft__(self):
""" The __ft__ method renders the dataclass at runtime."""
return Div(H1(self.title),P(self.statement), cls="hero")
# usage example
Main(
Hero("Hello World", "This is a hero statement")
)
# Usage example
@rt("/")
def get():
return Titled("FastHTML is awesome",
P("The fastest way to create web apps in Python"))
print(client.get("/").text)
<!doctype html>
<html>
<head>
<title>FastHTML is awesome</title> </head>
<body>
<main class="container"> <h1>FastHTML is awesome</h1>
<p>The fastest way to create web apps in Python</p>
</main> </body>
</html>
Forms
To validate data coming from users, first define a dataclass representing the data
you want to check. Here’s an example representing a signup form.
@dataclass
class Profile: email:str; phone:str; age:int
Once the dataclass and form function are completed, we can add data to the form. To
do that, instantiate the profile dataclass:
fill_form(profile_form, profile)
db = database("profiles.db")
profiles = db.create(Profile, pk="email")
profiles.insert(profile)
@rt("/profile/{email}")
def profile(email:str):
1 profile = profiles[email]
2 filled_profile_form = fill_form(profile_form, profile)
return Titled(f'Profile for {profile.email}', filled_profile_form)
print(client.get(f"/profile/[email protected]").text)
1
Fetch the profile using the profile table’s email primary key
2
Fill the form for display.
<!doctype html>
<html>
<head>
<title>Profile for [email protected]</title> </head>
<body>
<main class="container"> <h1>Profile for [email protected]</h1>
<form enctype="multipart/form-data" method="post"
action="/profile"><fieldset><label>Email <input name="email"
value="[email protected]">
</label><label>Phone <input name="phone" value="123456789">
</label><label>Age <input name="age" value="5">
</label></fieldset><button type="submit">Save</button></form></main> </body>
</html>
And now let’s demonstrate making a change to the data.
@rt("/profile")
1def post(profile: Profile):
2 profiles.update(profile)
3 return RedirectResponse(url=f"/profile/{profile.email}")
1
We use the Profile dataclass definition to set the type for the incoming profile
content. This validates the field types for the incoming data
2
Taking our validated data, we updated the profiles table
3
We redirect the user back to their profile view
4
The display is of the profile form view showing the changes in data.
<!doctype html>
<html>
<head>
<title>Profile for [email protected]</title> </head>
<body>
<main class="container"> <h1>Profile for [email protected]</h1>
<form enctype="multipart/form-data" method="post"
action="/profile"><fieldset><label>Email <input name="email"
value="[email protected]">
</label><label>Phone <input name="phone" value="7654321">
</label><label>Age <input name="age" value="25">
</label></fieldset><button type="submit">Save</button></form></main> </body>
</html>
Strings and conversion order
The general rules for rendering are: - __ft__ method will be called (for default
components like P, H2, etc. or if you define your own components) - If you pass a
string, it will be escaped - On other python objects, str() will be called
As a consequence, if you want to include plain HTML tags directly into e.g. a Div()
they will get escaped by default (as a security measure to avoid code injections).
This can be avoided by using NotStr(), a convenient way to reuse python code that
returns already HTML. If you use pandas, you can use pandas.DataFrame.to_html() to
get a nice table. To include the output a FastHTML, wrap it in NotStr(), like
Div(NotStr(df.to_html())).
Above we saw how a dataclass behaves with the __ft__ method defined. On a plain
dataclass, str() will be called (but not escaped).
@dataclass
class Hero:
title: str
statement: str
<div><div><h1>Some HTML</h1></div></div>
app, rt = fast_app(exception_handlers=exception_handlers)
@rt('/')
def get():
return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
serve()
exception_handlers={
404: lambda req, exc: Titled("404: I don't exist!"),
418: lambda req, exc: Titled("418: I'm a teapot!")
}
app, rt = fast_app(exception_handlers=exception_handlers)
@rt('/')
def get():
return (Titled("Home page", P(A(href="/oops")("Click to generate 404 error"))))
serve()
Cookies
We can set cookies using the cookie() function. In our example, we’ll create a
timestamp cookie.
@rt("/settimestamp")
def get(req):
now = datetime.now()
return P(f'Set to {now}'), cookie('now', datetime.now())
HTML(client.get('/settimestamp').text)
Now let’s get it back using the same name for our parameter as the cookie name.
@rt('/gettimestamp')
def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
client.get('/gettimestamp').text
@rt('/adder/{num}')
def get(session, num: int):
session.setdefault('sum', 0)
session['sum'] = session.get('sum') + num
return Response(f'The sum is {session["sum"]}.')
info
success
warning
error
Examples toasts might include:
“Payment accepted”
“Data submitted”
“Request approved”
Toasts require the use of the setup_toasts() function plus every view needs these
two features:
@rt('/toasting')
2def get(session):
# Normally one toast is enough, this allows us to see
# different toast types in action.
add_toast(session, f"Toast is being cooked", "info")
add_toast(session, f"Toast is ready", "success")
add_toast(session, f"Toast is getting a bit crispy", "warning")
add_toast(session, f"Toast is burning!", "error")
3 return Titled("I like toast")
1
setup_toasts is a helper function that adds toast dependencies. Usually this would
be declared right after fast_app()
2
Toasts require sessions
3
Views with Toasts must return FT or FtResponse components.
💡 setup_toasts takes a duration input that allows you to specify how long a toast
will be visible before disappearing. For example setup_toasts(duration=5) sets the
toasts duration to 5 seconds. By default toasts disappear after 10 seconds.
Now we pass our user_auth_before function as the first argument into a Beforeware
class. We also pass a list of regular expressions to the skip argument, designed to
allow users to still get to the home and login pages.
beforeware = Beforeware(
user_auth_before,
skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login', '/']
)
app, rt = fast_app(before=beforeware)
FastHTML introduces several tools for working with SSE which are covered in the
example below. While concise, there’s a lot going on in this function so we’ve
annotated it quite a bit.
import random
from asyncio import sleep
from fasthtml.common import *
1hdrs=(Script(src="https://unpkg.com/[email protected]/sse.js"),)
app,rt = fast_app(hdrs=hdrs)
@rt
def index():
return Titled("SSE Random Number Generator",
P("Generate pairs of random numbers, as the list grows scroll downwards."),
2 Div(hx_ext="sse",
3 sse_connect="/number-stream",
4 hx_swap="beforeend show:bottom",
5 sse_swap="message"))
6shutdown_event = signal_shutdown()
@rt("/number-stream")
10async def get(): return EventStream(number_generator())
1
Import the HTMX SSE extension
2
Tell HTMX to load the SSE extension
3
Look at the /number-stream endpoint for SSE content
4
When new items come in from the SSE endpoint, add them at the end of the current
content within the div. If they go beyond the screen, scroll downwards
5
Specify the name of the event. FastHTML’s default event name is “message”. Only
change if you have more than one call to SSE endpoints within a view
6
Set up the asyncio event loop
7
Don’t forget to make this an async function!
8
Iterate through the asyncio event loop
9
We yield the data. Data ideally should be comprised of FT components as that plugs
nicely into HTMX in the browser
10
The endpoint view needs to be an async function that returns a EventStream
Websockets
With websockets we can have bi-directional communications between a browser and
client. Websockets are useful for things like chat and certain types of games.
While websockets can be used for single direction messages from the server (i.e.
telling users that a process is finished), that task is arguably better suited for
SSE.
1app, rt = fast_app(exts='ws')
1
To use websockets in FastHTML, you must instantiate the app with exts set to ‘ws’
2
As we want to use websockets to reset the form, we define the mk_input function
that can be called from multiple locations
3
We create the form and mark it with the ws_send attribute, which is documented here
in the HTMX websocket specification. This tells HTMX to send a message to the
nearest websocket based on the trigger for the form element, which for forms is
pressing the enter key, an action considered to be a form submission
4
This is where the HTMX extension is loaded (hx_ext='ws') and the nearest websocket
is defined (ws_connect='/ws')
5
When a websocket first connects we can optionally have it call a function that
accepts a send argument. The send argument will push a message to the browser.
6
Here we use the send function that was passed into the on_connect function to send
a Div with an id of notifications that HTMX assigns to the element in the page that
already has an id of notifications
7
When a websocket disconnects we can call a function which takes no arguments.
Typically the role of this function is to notify the server to take an action. In
this case, we print a simple message to the console
8
We use the app.ws decorator to mark that /ws is the route for our websocket. We
also pass in the two optional conn and disconn parameters to this decorator. As a
fun experiment, remove the conn and disconn arguments and see what happens
9
Define the ws function as async. This is necessary for ASGI to be able to serve
websockets. The function accepts two arguments, a msg that is user input from the
browser, and a send function for pushing data back to the browser
10
The send function is used here to send HTML back to the page. As the HTML has an id
of notifications, HTMX will overwrite what is already on the page with the same ID
11
The websocket function can also be used to return a value. In this case, it is a
tuple of two HTML elements. HTMX will take the elements and replace them where
appropriate. As both have id specified (notifications and msg respectively), they
will replace their predecessor on the page.
File Uploads
A common task in web development is uploading files. The examples below are for
uploading files to the hosting server, with information about the uploaded file
presented to the user.
app, rt = fast_app()
upload_dir = Path("filez")
upload_dir.mkdir(exist_ok=True)
@rt('/')
def get():
return Titled("File Upload Demo",
Article(
1 Form(hx_post=upload, hx_target="#result-one")(
2 Input(type="file", name="file"),
Button("Upload", type="submit", cls='secondary'),
),
Div(id="result-one")
)
)
def FileMetaDataCard(file):
return Article(
Header(H3(file.filename)),
Ul(
Li('Size: ', file.size),
Li('Content Type: ', file.content_type),
Li('Headers: ', file.headers),
)
)
@rt
3async def upload(file: UploadFile):
4 card = FileMetaDataCard(file)
5 filebuffer = await file.read()
6 (upload_dir / file.filename).write_bytes(filebuffer)
return card
serve()
1
Every form rendered with the Form FT component defaults to enctype="multipart/form-
data"
2
Don’t forget to set the Input FT Component’s type to file
3
The upload view should receive a Starlette UploadFile type. You can add other form
variables
4
We can access the metadata of the card (filename, size, content_type, headers), a
quick and safe process. We set that to the card variable
5
In order to access the contents contained within a file we use the await method to
read() it. As files may be quite large or contain bad data, this is a seperate step
from accessing metadata
6
This step shows how to use Python’s built-in pathlib.Path library to write the file
to disk.
Multiple File Uploads
from fasthtml.common import *
from pathlib import Path
app, rt = fast_app()
upload_dir = Path("filez")
upload_dir.mkdir(exist_ok=True)
@rt('/')
def get():
return Titled("Multiple File Upload Demo",
Article(
1 Form(hx_post=upload_many, hx_target="#result-many")(
2 Input(type="file", name="files", multiple=True),
Button("Upload", type="submit", cls='secondary'),
),
Div(id="result-many")
)
)
def FileMetaDataCard(file):
return Article(
Header(H3(file.filename)),
Ul(
Li('Size: ', file.size),
Li('Content Type: ', file.content_type),
Li('Headers: ', file.headers),
)
)
@rt
3async def upload_many(files: list[UploadFile]):
cards = []
4 for file in files:
5 cards.append(FileMetaDataCard(file))
6 filebuffer = await file.read()
7 (upload_dir / file.filename).write_bytes(filebuffer)
return cards
serve()
1
Every form rendered with the Form FT component defaults to enctype="multipart/form-
data"
2
Don’t forget to set the Input FT Component’s type to file and assign the multiple
attribute to True
3
The upload view should receive a list containing the Starlette UploadFile type. You
can add other form variables
4
Iterate through the files
5
We can access the metadata of the card (filename, size, content_type, headers), a
quick and safe process. We add that to the cards variable
6
In order to access the contents contained within a file we use the await method to
read() it. As files may be quite large or contain bad data, this is a seperate step
from accessing metadata
7
This step shows how to use Python’s built-in pathlib.Path library to write the file
to disk.
---
https://docs.fastht.ml/tutorials/e2e.html
JS App Walkthrough
How to build a website with custom JavaScript in FastHTML step-by-step
Installation
You’ll need the following software to complete the tutorial, read on for specific
installation instructions:
Python
A Python package manager such as pip (which normally comes with Python) or uv
FastHTML
Web browser
Railway.app account
If you haven’t worked with Python before, we recommend getting started with
Miniconda.
Note that you will only need to follow the steps in the installation section once
per environment. If you create a new repo, you won’t need to redo these.
Install FastHTML
For Mac, Windows and Linux, enter:
First steps
By the end of this section you’ll have your own FastHTML website with tests
deployed to railway.app.
main.py
from fasthtml.common import *
app = FastHTML()
rt = app.route
@rt('/')
def get():
return 'Hello, world!'
serve()
Finally, run python main.py in your terminal and open your browser to the ‘Link’
that appears.
QuickDraw
Drawing Rooms
Drawing rooms are the core concept of our application. Each room represents a
separate drawing space where a user can let their inner Picasso shine. Here’s a
detailed breakdown:
@patch
def __ft__(self:Room):
return Li(A(self.name, href=f"/rooms/{self.id}"))
Or you can use our fast_app function to create a FastHTML app with a SQLite
database and dataclass in one line:
main.py
def render(room):
return Li(A(room.name, href=f"/rooms/{room.id}"))
We are specifying a render function to convert our dataclass into HTML, which is
the same as extending the __ft__ method from the patch decorator we used before. We
will use this method for the rest of the tutorial since it is a lot cleaner and
easier to read.
@rt("/rooms")
async def post(room:Room):
room.created_at = datetime.now().isoformat()
return rooms.insert(room)
When a user submits the “Create Room” form, this route is called.
It creates a new Room object, sets the creation time, and inserts it into the
database.
It returns an HTML list item with a link to the new room, which is dynamically
added to the room list on the homepage thanks to HTMX.
Let’s give our rooms shape
main.py
@rt("/rooms/{id}")
async def get(id:int):
room = rooms[id]
return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"),
A(Button("Leave Room"), href="/"))
main.py
from fasthtml.common import *
from datetime import datetime
def render(room):
return Li(A(room.name, href=f"/rooms/{room.id}"))
@rt("/")
def get():
create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
Button("Create Room"),
hx_post="/rooms", hx_target="#rooms-list",
hx_swap="afterbegin")
rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
return Titled("DrawCollab", create_room, rooms_list)
@rt("/rooms")
async def post(room:Room):
room.created_at = datetime.now().isoformat()
return rooms.insert(room)
@rt("/rooms/{id}")
async def get(id:int):
room = rooms[id]
return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"),
A(Button("Leave Room"), href="/"))
serve()
Now run python main.py in your terminal and open your browser to the ‘Link’ that
appears. You should see a page with a form to create a new room and a list of
existing rooms.
main.py
# ... (keep the previous imports and database setup)
@rt("/rooms/{id}")
async def get(id:int):
room = rooms[id]
canvas = Canvas(id="canvas", width="800", height="600")
color_picker = Input(type="color", id="color-picker", value="#3CDD8C")
brush_size = Input(type="range", id="brush-size", min="1", max="50",
value="10")
js = """
var canvas = new fabric.Canvas('canvas');
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = '#3CDD8C';
canvas.freeDrawingBrush.width = 10;
document.getElementById('color-picker').onchange = function() {
canvas.freeDrawingBrush.color = this.value;
};
document.getElementById('brush-size').oninput = function() {
canvas.freeDrawingBrush.width = parseInt(this.value, 10);
};
"""
Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"),
Script(js))
Now we’ve got a drawing canvas! FastHTML makes it easy to include external
libraries and add custom JavaScript.
@rt("/rooms/{id}/load")
async def get(id:int):
room = rooms[id]
return room.canvas_data if room.canvas_data else "{}"
With these changes, users can now save their drawings and load them when they
return to the room. The canvas data is stored as a JSON string in the database,
allowing for easy serialization and deserialization. Try it out! Create a new room,
make a drawing, save it, and then reload the page. You should see your drawing
reappear, ready for further editing.
main.py
from fasthtml.common import *
from datetime import datetime
def render(room):
return Li(A(room.name, href=f"/rooms/{room.id}"))
app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str,
created_at=str, canvas_data=str, pk='id')
@rt("/")
def get():
create_room = Form(Input(id="name", name="name", placeholder="New Room Name"),
Button("Create Room"),
hx_post="/rooms", hx_target="#rooms-list",
hx_swap="afterbegin")
rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list')
return Titled("QuickDraw",
create_room, rooms_list)
@rt("/rooms")
async def post(room:Room):
room.created_at = datetime.now().isoformat()
return rooms.insert(room)
@rt("/rooms/{id}")
async def get(id:int):
room = rooms[id]
canvas = Canvas(id="canvas", width="800", height="600")
color_picker = Input(type="color", id="color-picker", value="#000000")
brush_size = Input(type="range", id="brush-size", min="1", max="50",
value="10")
save_button = Button("Save Canvas", id="save-canvas",
hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data:
JSON.stringify(canvas.toJSON())}")
js = f"""
var canvas = new fabric.Canvas('canvas');
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = '#000000';
canvas.freeDrawingBrush.width = 10;
document.getElementById('color-picker').onchange = function() {{
canvas.freeDrawingBrush.color = this.value;
}};
document.getElementById('brush-size').oninput = function() {{
canvas.freeDrawingBrush.width = parseInt(this.value, 10);
}};
"""
@rt("/rooms/{id}/save")
async def post(id:int, canvas_data:str):
rooms.update({'canvas_data': canvas_data}, id)
return "Canvas saved successfully"
@rt("/rooms/{id}/load")
async def get(id:int):
room = rooms[id]
return room.canvas_data if room.canvas_data else "{}"
serve()
Deploying to Railway
You can deploy your website to a number of hosting providers, for this tutorial
we’ll be using Railway. To get started, make sure you create an account and install
the Railway CLI. Once installed, make sure to run railway login to log in to your
account.
To make deploying your website as easy as possible, FastHTMl comes with a built in
CLI tool that will handle most of the deployment process for you. To deploy your
website, run the following command in your terminal in the root directory of your
project:
fh_railway_deploy quickdraw
Note
Your app must be located in a main.py file for this to work.
FastHTML allows you to create dynamic web apps with minimal code.
We used FastHTML’s routing system to handle different pages and actions.
We integrated with a SQLite database to store room information and canvas data.
We utilized Fabric.js to create an interactive drawing canvas.
We implemented features like color picking, brush size adjustment, and canvas
saving.
We used HTMX for seamless, partial page updates without full reloads.
We learned how to deploy our FastHTML application to Railway for easy hosting.
You’ve taken your first steps into the world of FastHTML development. From here,
the possibilities are endless! You could enhance the drawing app further by adding
features like:
---
https://docs.fastht.ml/tutorials/jupyter_and_fasthtml.html
The first step is to import necessary libraries. As using FastHTML inside a Jupyter
notebook is a special case, it remains a special import.
app, rt = fast_app(pico=True)
@rt
def index():
return Titled('Hello, Jupyter',
P('Welcome to the FastHTML + Jupyter example'),
Button('Click', hx_get='/click', hx_target='#dest'),
Div(id='dest')
)
Create a server object using JupyUvi, which also starts Uvicorn. The server runs in
a separate thread from Jupyter, so it can use normal HTTP client functions in a
notebook.
server = JupyUvi(app)
The HTMX callable displays the server’s HTMX application in an iframe which can be
displayed by Jupyter notebook. Pass in the same port variable used in the JupyUvi
callable above or leave it blank to use the default (8000).
# This doesn't display in the docs - uncomment and run it to see it in action
# HTMX()
We didn’t define the /click route, but that’s fine - we can define (or change) it
any time, and it’s dynamically inserted into the running app. No need to restart or
reload anything!
@rt
def click(): return P('You clicked me!')
Graceful shutdowns
Use the server.stop() function displayed below. If you restart Jupyter without
calling this line the thread may not be released and the HTMX callable above may
throw errors. If that happens, a quick temporary fix is to specify a different port
number in JupyUvi and HTMX with the port parameter.
Cleaner solutions to the dangling thread are to kill the dangling thread (dependant
on each operating system) or restart the computer.
server.stop()
---
https://docs.fastht.ml/explains/explaining_xt_components.html
FT Components
FT components turn Python objects into HTML.
FT, or ‘FastTags’, are the display components of FastHTML. In fact, the word
“components” in the context of FastHTML is often synonymous with FT.
For example, when we look at a FastHTML app, in particular the views, as well as
various functions and other objects, we see something like the code snippet below.
It’s the return statement that we want to pay attention to:
def example():
# The code below is a set of ft components
return Div(
H1("FastHTML APP"),
P("Let's do this"),
cls="go"
)
Let’s go ahead and call our function and print the result:
example()
<div class="go">
<h1>FastHTML APP</h1>
<p>Let's do this</p>
</div>
As you can see, when returned to the user from a Python callable, like a function,
the ft components are transformed into their string representations of XML or XML-
like content such as HTML. More concisely, ft turns Python objects into HTML.
Now that we know what ft components look and behave like we can begin to understand
them. At their most fundamental level, ft components:
ft components can be made from any callable type, so adhering to any one pattern
doesn’t make much sense
It makes for easier reading of FastHTML code, as anything that is PascalCase is
probably an ft component
Default FT components
FastHTML has over 150 FT components designed to accelerate web development. Most of
these mirror HTML tags such as <div>, <p>, <a>, <title>, and more. However, there
are some extra tags added, including:
ft.Ul(
ft.Li("one"),
ft.Li("two"),
ft.Li("three")
)
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
Attributes
This example demonstrates many important things to know about how ft components
handle attributes.
#| echo: False
1Label(
"Choose an option",
Select(
2 Option("one", value="1", selected=True),
3 Option("two", value="2", selected=False),
4 Option("three", value=3),
5 cls="selector",
6 _id="counter",
7 **{'@click':"alert('Clicked');"},
),
8 _for="counter",
)
1
Line 2 demonstrates that FastHTML appreciates Labels surrounding their fields.
2
On line 5, we can see that attributes set to the boolean value of True are rendered
with just the name of the attribute.
3
On line 6, we demonstrate that attributes set to the boolean value of False do not
appear in the rendered output.
4
Line 7 is an example of how integers and other non-string values in the rendered
output are converted to strings.
5
Line 8 is where we set the HTML class using the cls argument. We use cls here as
class is a reserved word in Python. During the rendering process this will be
converted to the word “class”.
6
Line 9 demonstrates that any named argument passed into an ft component will have
the leading underscore stripped away before rendering. Useful for handling reserved
words in Python.
7
On line 10 we have an attribute name that cannot be represented as a python
variable. In cases like these, we can use an unpacked dict to represent these
values.
8
The use of _for on line 12 is another demonstration of an argument having the
leading underscore stripped during render. We can also use fr as that will be
expanded to for.
This renders the following HTML snippet:
Label(
"Choose an option",
Select(
Option("one", value="1", selected=True),
Option("two", value="2", selected=False),
Option("three", value=3), # <4>,
cls="selector",
_id="counter",
**{'@click':"alert('Clicked');"},
),
_for="counter",
)
<label for="counter">
Choose an option
<select id="counter" @click="alert('Clicked');" class="selector"
name="counter">
<option value="1" selected>one</option>
<option value="2" >two</option>
<option value="3">three</option>
</select>
</label>
For more information, see the Defining new ft components reference page.
The reason is that FastHTML leverages python’s dynamic features to a great degree.
Especially when it comes to FT components, which can evaluate out to be FT|str|
None|tuple as well as anything that supports the __ft__, __html__, and __str__
method. That’s enough of the Python stack that assigning anything but Any to be the
FT type will prove an exercise in frustation.
---
https://docs.fastht.ml/explains/faq.html
FAQ
Frequently Asked Questions
Why does my editor say that I have errors in my FastHTML code?
Many editors, including Visual Studio Code, use PyLance to provide error checking
for Python. However, PyLance’s error checking is just a guess – it can’t actually
know whether your code is correct or not. PyLance particularly struggles with
FastHTML’s syntax, which leads to it often reporting false error messages in
FastHTML projects.
To avoid these misleading error messages, it’s best to disable some PyLance error
checking in your FastHTML projects. Here’s how to do it in Visual Studio Code (the
same approach should also work in other editors based on vscode, such as Cursor and
GitHub Codespaces):
{
"python.analysis.ignore": [ "*" ]
}
If you are coming from a data science background the fastai coding style may
already be your preferred style.
If you are coming from a PEP-8 background where the use of ruff is encouraged,
there is a learning curve. However, once you get used to the fastai coding style
you may discover yourself appreciating the concise nature of this style. It also
encourages using more functional programming tooling, which is both productive and
fun. Having said that, it’s entirely optional!
We wrote some more thoughts on Why Python HTML components over Jinja2, Mako, or JSX
here.
Second, our style lends itself to working in rather compact Jupyter notebooks and
small Python modules. Hence we know about the source code whose libraries we import
* from. This terseness means we can develop faster. We’re a small team, and any
edge we can gain is important to us.
We’ll finish by saying a lot of our users employ explicit imports. If that’s the
path you want to take, we encourage the use of from fasthtml import common as fh.
The acronym of fh makes it easy to recognize that a symbol is from the FastHTML
library.
Watch this video. We’ve used Jupyter notebooks exported via nbdev to write a wide
range of “very serious” software projects over the last three years. This includes
deep learning libraries, API clients, Python language extensions, terminal user
interfaces, web frameworks, and more!
The nbdev project spent around a year trying to move to pyproject.toml but there
was insufficient functionality in the toml-based approach to complete the
transition.
---
https://docs.fastht.ml/explains/minidataapi.html
MiniDataAPI Spec
The MiniDataAPI is a persistence API specification that designed to be small and
relatively easy to implement across a wide range of datastores. While early
implementations have been SQL-based, the specification can be quickly implemented
in key/value stores, document databases, and more.
Work in Progress
The MiniData API spec is a work in progress, subject to change. While the majority
of design is complete, expect there could be breaking changes.
Why?
The MiniDataAPI specification allows us to use the same API for many different
database engines. Any application using the MiniDataAPI spec for interacting with
its database requires no modification beyond import and configuration changes to
switch database engines. For example, to convert an application from Fastlite
running SQLite to FastSQL running PostgreSQL, should require only changing these
two lines:
FastLite version
FastSQL version
As both libraries adhere to the MiniDataAPI specification, the rest of the code in
the application should remain the same. The advantage of the MiniDataAPI spec is
that it allows people to use whatever datastores they have access to or prefer.
Note
Switching databases won’t migrate any existing data between databases.
– Jeremy Howard
This means the specification does not include joins or formal foreign keys. Complex
data stored over multiple tables that require joins isn’t handled well. For this
kind of scenario it’s probably for the best to use more sophisticated ORMs or even
direct database queries.
db = database(':memory:')
Here’s a complete list of the available methods in the API, all documented below
(assuming db is a database and t is a table):
db.create
t.insert
t.delete
t.update
t[key]
t(...)
t.xtra
Tables
For the sake of expediency, this document uses a SQL example. However, tables can
represent anything, not just the fundamental construct of a SQL databases. They
might represent keys within a key/value structure or files on a hard-drive.
Creating tables
We use a create() method attached to Database object (db in our example) to create
the tables.
@dataclass
class Todo: id: int; title: str; detail: str; status: str; name: str
todos = db.create(Todo)
todos
Transforming tables
Depending on the database type, this method can include transforms - the ability to
modify the tables. Let’s go ahead and add a password field for our table called
pwd.
.insert()
Add a new record to the database. We want to support as many types as possible, for
now we have tests for Python classes, dataclasses, and dicts. Returns an instance
of the new record.
user = users['Alma']
user
try: users['David']
except NotFoundError: print(f'User not found')
todos[1]
publications[['Alma', 2019]]
publications['Alma', 2030]
users()
users(order_by='name')
users(where="name='Alma'")
users("name=?", ('Alma',))
users(limit=1)
users(limit=5, offset=1)
user
users.update(name='Alma', year_started=2149)
John hasn’t started with us yet so doesn’t get the chance yet to travel in time.
users.delete('Charlie')
try: users.delete('Charlies')
except NotFoundError: print('User not found')
try: users.delete('John')
except NotFoundError: print('User not found')
publications.delete(['Alma' , 2035])
(True, False)
Also works with compound primary keys, as shown below. You’ll note that the
operation can be done with either a list or tuple.
True
And now for a False result, where John has no publications.
False
.xtra()
If we set fields within the .xtra function to a particular value, then indexing is
also filtered by those. This applies to every database method except for record
creation. This makes it easier to limit users (or other objects) access to only
things for which they have permission. This is a one-way operation, once set it
can’t be undone for a particular table object.
For example, if we query all our records below without setting values via the .xtra
function, we can see todos for everyone. Pay special attention to the id values of
all three records, as we are about to filter most of them away.
todos()
todos.xtra(name='Charlie')
We’ve now set a field to a value with .xtra, if we loop over all the records again,
only those assigned to records with a name of Charlie will be displayed.
todos()
ct = todos[3]
ct
ct.id in todos
True
If we try in with the other IDs the query fails because the filtering is now set to
just records with a name of Charlie.
1 in todos, 2 in todos
(False, False)
try: todos[2]
except NotFoundError: print('Record not found')
try: todos.delete(1)
except NotFoundError as e: print('Record not updated')
todos.delete(ct.id)
ct.name = 'Braden'
todos.update(ct)
users = db.t.user
users
From the table objects we can extract a Dataclass version of our tables. Usually
this is given an singular uppercase version of our table name, which in this case
is User.
User = users.dataclass()
Implementations
fastlite - The original implementation, only for Sqlite
fastsql - An SQL database agnostic implementation based on the excellent SQLAlchemy
library.
---
https://docs.fastht.ml/explains/oauth.html
OAuth
OAuth is an open standard for ‘access delegation’, commonly used as a way for
Internet users to grant websites or applications access to their information on
other websites but without giving them the passwords. It is the mechanism that
enables “Log in with Google” on many sites, saving you from having to remember and
manage yet another password. Like many auth-related topics, there’s a lot of depth
and complexity to the OAuth standard, but once you understand the basic usage it
can be a very convenient alternative to managing your own user accounts.
On this page you’ll see how to use OAuth with FastHTML to implement some common
pieces of functionality.
Creating an Client
FastHTML has Client classes for managing settings and state for different OAuth
providers. Currently implemented are: GoogleAppClient, GitHubAppClient,
HuggingFaceClient and DiscordAppClient - see the source if you need to add other
providers. You’ll need a client_id and client_secret from the provider (see the
from-scratch example later in this page for an example of registering with GitHub)
to create the client. We recommend storing these in environment variables, rather
than hardcoding them in your code.
import os
from fasthtml.oauth import GoogleAppClient
client = GoogleAppClient(os.getenv("AUTH_CLIENT_ID"),
os.getenv("AUTH_CLIENT_SECRET"))
The client is used to obtain a login link and to manage communications between your
app and the OAuth provider (client.login_link(redirect_uri="/redirect")).
class Auth(OAuth):
def get_auth(self, info, ident, session, state):
email = info.email or ''
if info.email_verified and email.split('@')[-1]=='answer.ai':
return RedirectResponse('/', status_code=303)
app = FastHTML()
oauth = Auth(app, client)
@app.get('/')
def home(auth): return P('Logged in!'), A('Log out', href='/logout')
@app.get('/login')
def login(req): return Div(P("Not logged in"), A('Log in',
href=oauth.login_link(req)))
There’s a fair bit going on here, so let’s unpack what’s happening in that code:
OAuth (and by extension our custom Auth class) has a number of default arguments,
including some key URLs: redir_path='/redirect', error_path='/error',
logout_path='/logout', login_path='/login'. It will create and handle the redirect
and logout paths, and it’s up to you to handle /login (where unsuccessful login
attempts will be redirected) and /error (for oauth errors).
When we run oauth = Auth(app, client) it adds the redirect and logout paths to the
app and also adds some beforeware. This beforeware runs on any requests (apart from
any specified with the skip parameter).
The added beforeware specifies some app behaviour:
If someone who isn’t logged in attempts to visit our homepage (/) here, they will
be redirected to /login.
If they are logged in, it calls a check_invalid method. This defaults to False,
which let’s the user continue to the page they requested. The behaviour can be
modified by defining your own check_invalid method in the Auth class - for example,
you could have this forcibly log out users who have recently been banned.
So how does someone log in? If they visit (or are redirected to) the login page
at /login, we show them a login link. This sends them to the OAuth provider, where
they’ll go through the steps of selecting their account, giving permissions etc.
Once done they will be redirected back to /redirect. Behind the scenes a code that
comes as part of their request gets turned into user info, which is then passed to
the key function get_auth(self, info, ident, session, state). Here is where you’d
handle looking up or adding a user in a database, checking for some condition (for
example, this code checks if the email is an answer.ai email address) or choosing
the destination based on state. The arguments are:
self: the Auth object, which you can use to access the client (self.cli)
info: the information provided by the OAuth provider, typically including a unique
user id, email address, username and other metadata.
ident: a unique identifier for this user. What this looks like varies between
providers. This is useful for managing a database of users, for example.
session: the current session, that you can store information in securely
state: you can optionally pass in some state when creating the login link. This
persists and is returned after the user goes through the Oath steps, which is
useful for returning them to the same page they left. It can also be used as added
security against CSRF attacks.
In our example, we check the email in info (we use a GoogleAppClient, not all
providers will include an email). If we aren’t happy, and get_auth returns False or
nothing (as in the case here for non-answerai people) then the user is redirected
back to the login page. But if everything looks good we return a redirect to the
homepage, and an auth key is added to the session and the scope containing the
users identity ident. So, for example, in the homepage route we could use auth to
look up this particular user’s profile info and customize the page accordingly.
This auth will persist in their session until they clear the browser cache, so by
default they’ll stay logged in. To log them out, remove it ( session.pop('auth',
None)) or send them to /logout which will do that for you.
If you’re wanting to learn more about how this works, and to see where you might
add additional functionality, the rest of this page will walk through some examples
without the OAuth convenience class, to illustrate the concepts. This was writted
before said OAuth class was available, and is kep here for educational purposes -
we recommend you stick with the new approach shown above in most cases.
OAuth requires a “provider” (in this case, GitHub) to authenticate the user. So the
first step when setting up our app is to register with GitHub to set things up.
client = GitHubAppClient(
client_id="your_client_id",
client_secret="your_client_secret"
)
You should also save the path component of the authorization callback URL which you
provided on registration.
This route is where GitHub will redirect the user’s browser in order to send an
authorization code to your app. You should save only the URL’s path component
rather than the entire URL because you want your code to work automatically in
deployment, when the host and port part of the URL change from localhost:8000 to
your real DNS name.
auth_callback_path = "/auth_redirect"
Note
It’s recommended to store the client ID, and secret, in environment variables,
rather than hardcoding them in your code.
When the user visit a normal page of your app, if they are not already logged in,
then you’ll want to redirect them to your app’s login page, which will live at
the /login path. We accomplish that by using this piece of “beforeware”, which
defines logic which runs before other work for all routes except ones we specify to
be skipped:
We configure the beforeware to skip /login because that’s where the user goes to
login, and we also skip the special authorization callback path because that is
used by OAuth itself to receive information from GitHub.
It’s only at your login page that we start the OAuth flow. To start the OAuth flow,
you need to give the user a link to GitHub’s login for your app. You’ll need the
client object to generate that link, and the client object will in turn need the
full authorization callback URL, which we need to build from the authorization
callback path, so it is a multi-step process to produce this GitHub login link.
Here is an implementation of your own /login route handler. It generates the GitHub
login link and presents it to the user:
@app.get('/login')
def login(request)
redir = redir_url(request,auth_callback_path)
login_link = client.login_link(redir)
return P(A('Login with GitHub', href=login_link))
Once the user follows that link, GitHub will ask them to grant permission to your
app to access their GitHub account. If they agree, GitHub will redirect them back
to your app’s authorization callback URL, carrying an authorization code which your
app can use to generate an access token. To receive this code, you need to set up a
route in FastHTML that listens for requests at the authorization callback path. For
example:
@app.get(auth_callback_path)
def auth_redirect(code:str):
return P(f"code: {code}")
This authorization code is temporary, and is used by your app to directly ask the
provider for user information like an access token.
Us to GitHUb: “A user just gave me this auth code. May I have the user info (e.g.,
an access token)?”
GitHub to us: “Since you have an auth code, here’s the user info”
It’s critical for us to derive the user info from the auth code immediately in the
authorization callback, because the auth code may be used only once. So we use it
that once in order to get information like an access token, which will remain valid
for longer.
@app.get(auth_callback_path)
def auth_redirect(code:str, request):
redir = redir_url(request, auth_callback_path)
user_info = client.retr_info(code, redir)
user_id = info[client.id_key]
return P(f"User id: {user_id}")
But we want the user ID not to print it but to remember the user.
@app.get(auth_callback_path)
def auth_redirect(code:str, request, session):
redir = redir_url(request, auth_callback_path)
user_info = client.retr_info(code, redir)
user_id = user_info[client.id_key] # get their ID
session['user_id'] = user_id # save ID in the session
return RedirectResponse('/', status_code=303)
The session object is derived from values visible to the user’s browser, but it is
cryptographically signed so the user can’t read it themselves. This makes it safe
to store even information we don’t want to expose to the user.
For larger quantities of data, we’d want to save that information in a database and
use the session to hold keys to lookup information from that database.
Here’s a minimal app that puts all these pieces together. It uses the user info to
get the user_id. It stores that in the session object. It then uses the user_id as
a key into a database, which tracks how frequently every user has hit an increment
button.
import os
from fasthtml.common import *
from fasthtml.oauth import GitHubAppClient, redir_url
db = database('data/counts.db')
counts = db.t.counts
if counts not in db.t: counts.create(dict(name=str, count=int), pk='name')
Count = counts.dataclass()
app = FastHTML(before=bware)
@app.get('/')
def home(auth):
return Div(
P("Count demo"),
P(f"Count: ", Span(counts[auth].count, id='count')),
Button('Increment', hx_get='/increment', hx_target='#count'),
P(A('Logout', href='/logout'))
)
@app.get('/increment')
def increment(auth):
c = counts[auth]
c.count += 1
return counts.upsert(c).count
@app.get('/logout')
def logout(session):
session.pop('user_id', None)
return RedirectResponse('/login', status_code=303)
serve()
The before function is used to check if the user is authenticated. If not, they are
redirected to the login page.
To log the user out, we remove the user ID from the session.
Calling counts.xtra(name=auth) ensures that only the row corresponding to the
current user is accessible when responding to a request. This is often nicer than
trying to remember to filter the data in every route, and lowers the risk of
accidentally leaking data.
In the auth_redirect route, we store the user ID in the session and create a new
row in the user_counts table if it doesn’t already exist.
You can find more heavily-commented version of this code in the oauth directory in
fasthtml-example, along with an even more minimal example. More examples may be
added in the future.
As a user, you can usually revoke access to an app from the provider’s website (for
example, https://github.com/settings/applications). But as a developer, you can
also revoke access programmatically - at least with some providers. This requires
keeping track of the access token (stored in client.token["access_token"] after you
call retr_info), and sending a request to the provider’s revoke URL:
auth_revoke_url = "https://accounts.google.com/o/oauth2/revoke"
def revoke_token(token):
response = requests.post(auth_revoke_url, params={"token": token})
return response.status_code == 200 # True if successful
Not all providers support token revocation, and it is not built into FastHTML
clients at the moment.
# in login page:
link = A('Login with GitHub',
href=client.login_link_with_state(state='current_prompt: add a unicorn'))
# in auth_redirect:
@app.get('/auth_redirect')
def auth_redirect(code:str, session, state:str=None):
print(f"state: {state}") # Use as needed
...
The state string is passed through the OAuth flow and back to your site.
A Work in Progress
This page (and OAuth support in FastHTML) is a work in progress. Questions, PRs,
and feedback are welcome!
---
https://docs.fastht.ml/explains/routes.html
Routes
Behaviour in FastHTML apps is defined by routes. The syntax is largely the same as
the wonderful FastAPI (which is what you should be using instead of this if you’re
creating a JSON service. FastHTML is mainly for making HTML web apps, not APIs).
Unfinished
We haven’t yet written complete documentation of all of FastHTML’s routing features
– until we add that, the best place to see all the available functionality is to
look over the tests
Note that you need to include the types of your parameters, so that FastHTML knows
what to pass to your function. Here, we’re just expecting a string:
app = FastHTML()
@app.get('/user/{nm}')
def get_nm(nm:str): return f"Good day to you, {nm}!"
Normally you’d save this into a file such as main.py, and then run it in uvicorn
using:
uvicorn main:app
However, for testing, we can use Starlette’s TestClient to try it out:
client = TestClient(app)
r = client.get('/user/Jeremy')
r
r.text
rt = app.route
@rt('/')
def post(): return "Going postal!"
client.post('/').text
'Going postal!'
Route-specific functionality
FastHTML supports custom decorators for adding specific functionality to routes.
This allows you to implement authentication, authorization, middleware, or other
custom behaviors for individual routes.
def basic_auth(f):
@wraps(f)
async def wrapper(req, *args, **kwargs):
token = req.headers.get("Authorization")
if token == 'abc123':
return await f(req, *args, **kwargs)
return Response('Not Authorized', status_code=401)
return wrapper
@app.get("/protected")
@basic_auth
async def protected(req):
return "Protected Content"
'Protected Content'
The decorator intercepts the request before the route function executes. If the
decorator allows the request to proceed, it calls the original route function,
passing along the request and any other arguments.
One of the key advantages of this approach is the ability to apply different
behaviors to different routes. You can also stack multiple decorators on a single
route for combined functionality.
def app_beforeware():
print('App level beforeware')
app = FastHTML(before=Beforeware(app_beforeware))
client = TestClient(app)
def route_beforeware(f):
@wraps(f)
async def decorator(*args, **kwargs):
print('Route level beforeware')
return await f(*args, **kwargs)
return decorator
def second_route_beforeware(f):
@wraps(f)
async def decorator(*args, **kwargs):
print('Second route level beforeware')
return await f(*args, **kwargs)
return decorator
@app.get("/users")
@route_beforeware
@second_route_beforeware
async def users():
return "Users Page"
client.get('/users').text
Combining Routes
Sometimes a FastHTML project can grow so weildy that putting all the routes into
main.py becomes unweildy. Or, we install a FastHTML- or Starlette-based package
that requires us to add routes.
First let’s create a books.py module, that represents all the user-related views:
# books.py
books_app, rt = fast_app()
@rt("/", name="list")
def get():
return Titled("Books", *[P(book) for book in books])
serve()
1
We use starlette.Mount to add the route to our routes list. We provide the name of
books to make discovery and management of the links easier. More on that in items 2
and 3 of this annotations list
2
This example link to the books list view is hand-crafted. Obvious in purpose, it
makes changing link patterns in the future harder
3
This example link uses the named URL route for the books. The advantage of this
approach is it makes management of large numbers of link items easier.
---
https://docs.fastht.ml/explains/websockets.html
WebSockets
Websockets are a protocol for two-way, persistent communication between a client
and server. This is different from HTTP, which uses a request/response model where
the client sends a request and the server responds. With websockets, either party
can send messages at any time, and the other party can respond.
This allows for different applications to be built, including things like chat
apps, live-updating dashboards, and real-time collaborative tools, which would
require constant polling of the server for updates with HTTP.
In FastHTML, you can create a websocket route using the @app.ws decorator. This
decorator takes a route path, and optional conn and disconn parameters representing
the on_connect and on_disconnect callbacks in websockets, respectively. The
function decorated by @app.ws is the main function that is called when a message is
received.
The on_message function is the main function that is called when a message is
received and can be named however you like. Similar to standard routes, the
arguments to on_message are automatically parsed from the websocket payload for
you, so you don’t need to manually parse the message content. However, certain
argument names are reserved for special purposes. Here are the most important ones:
send is a function that can be used to send text data to the client.
data is a dictionary containing the data sent by the client.
ws is a reference to the websocket object.
For example, we can send a message to the client that just connected like this:
async def on_conn(send):
await send(Div('Hello, world!'))
Or if we receive a message from the client, we can send a message back to them:
On the client side, we can use HTMX’s websocket extension to open a websocket
connection and send/receive messages. For example:
app = FastHTML(exts='ws')
@app.get('/')
def home():
cts = Div(
Div(id='notifications'),
Form(Input(id='msg'), id='form', ws_send=True),
hx_ext='ws', ws_connect='/ws')
return Titled('Websocket Test', cts)
This will create a websocket connection to the server on route /ws, and send any
form submissions to the server via the websocket. The server will then respond by
sending a message back to the client. The client will then update the message div
with the message from the server using Out of Band Swaps, which means that the
content is swapped with the same id without reloading the page.
Note
Make sure you set exts='ws' when creating your FastHTML object if you want to use
websockets so the extension is loaded.
Putting it all together, the code for the client and server should look like this:
app = FastHTML(exts='ws')
rt = app.route
@rt('/')
def get():
cts = Div(
Div(id='notifications'),
Form(Input(id='msg'), id='form', ws_send=True),
hx_ext='ws', ws_connect='/ws')
return Titled('Websocket Test', cts)
@app.ws('/ws')
async def ws(msg:str, send):
await send(Div('Hello ' + msg, id='notifications'))
serve()
This is a fairly simple example and could be done just as easily with standard HTTP
requests, but it illustrates the basic idea of how websockets work. Let’s look at a
more complex example next.
app = FastHTML(exts='ws')
rt = app.route
@rt('/login')
def get(session):
session["person"] = "Bob"
return "ok"
@app.ws('/ws')
async def ws(msg:str, send, session):
await send(Div(f'Hello {session.get("person")}' + msg, id='notifications'))
serve()
app = FastHTML(exts='ws')
rt = app.route
msgs = []
@rt('/')
def home(): return Div(
Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
Form(Input(id='msg'), id='form', ws_send=True),
hx_ext='ws', ws_connect='/ws')
Now, let’s handle the websocket connection. We’ll add a new route for this along
with an on_conn and on_disconn function to keep track of the users currently
connected to the websocket. Finally, we will handle the logic for sending messages
to all connected users.
users = {}
def on_conn(ws, send): users[str(id(ws))] = send
def on_disconn(ws): users.pop(str(id(ws)), None)
serve()
We can now run this app with python chat_ws.py and open multiple browser tabs to
http://localhost:5001. You should be able to send messages in one tab and see them
appear in the other tabs.
A Work in Progress
This page (and Websocket support in FastHTML) is a work in progress. Questions,
PRs, and feedback are welcome!
---
https://docs.fastht.ml/ref/defining_xt_component.html
Custom Components
The majority of the time the default ft components are all you need (for example
Div, P, H1, etc.).
Pre-requisite Knowledge
If you don’t know what an ft component is, you should read the explaining ft
components explainer first.
However, there are many situations where you need a custom ft component that
creates a unique HTML tag (for example <zero-md></zero-md>). There are many options
in FastHTML to do this, and this section will walk through them. Generally you want
to use the highest level option that fits your needs.
Real-world example
This external tutorial walks through a practical situation where you may want to
create a custom HTML tag using a custom ft component. Seeing a real-world example
is a good way to understand why the contents of this guide is useful.
NotStr
The first way is to use the NotStr class to use an HTML tag as a string. It works
as a one-off but quickly becomes harder to work with as complexity grows. However
we can see that you can genenrate the same xml using NotStr as the out-of-the-box
components.
div_NotStr = NotStr('<div></div>')
print(div_NotStr)
<div></div>
Automatic Creation
The next (and better) approach is to let FastHTML generate the component function
for you. As you can see in our assert this creates a function that creates the HTML
just as we wanted. This works even though there is not a Some_never_before_used_tag
function in the fasthtml.components source code (you can verify this yourself by
looking at the source code).
Tip
Typically these tags are needed because a CSS or Javascript library created a new
XML tag that isn’t default HTML. For example the zero-md javascript library looks
for a <zero-md></zero-md> tag to know what to run its javascript code on. Most CSS
libraries work by creating styling based on the class attribute, but they can also
apply styling to an arbitrary HTML tag that they made up.
Some_never_before_used_tag()
<some-never-before-used-tag></some-never-before-used-tag>
Manual Creation
The automatic creation isn’t magic. It’s just calling a python function __getattr__
and you can call it yourself to get the same result.
import fasthtml
auto_called = fasthtml.components.Some_never_before_used_tag()
manual_called = fasthtml.components.__getattr__('Some_never_before_used_tag')()
Knowing that, we know that it’s possible to create a different function that has
different behavior than FastHTMLs default behavior by modifying how the
___getattr__ function creates the components! It’s only a few lines of code and
reading that what it does is a great way to understand components more deeply.
Tip
Dunder methods and functions are special functions that have double underscores at
the beginning and end of their name. They are called at specific times in python so
you can use them to cause customized behavior that makes sense for your specific
use case. They can appear magical if you don’t know how python works, but they are
extremely commonly used to modify python’s default behavior (__init__ is probably
the most common one).
For example if you want a component that creates <path></path> that doesn’t
conflict names with pathlib.Path you can do that. FastHTML automatically creates
new components with a 1:1 mapping and a consistent name, which is almost always
what you want. But in some cases you may want to customize that and you can use the
ft_hx function to do that differently than the default.
ft_path()
<path></path>
We can add any behavior in that function that we need to, so let’s go through some
progressively complex examples that you may need in some of your projects.
Underscores in tags
Now that we understand how FastHTML generates components, we can create our own in
all kinds of ways. For example, maybe we need a weird HTML tag that uses
underscores. FastHTML replaces _ with - in tags because underscores in tags are
highly unusual and rarely what you want, though it does come up rarely.
tag_with_underscores()
<tag_with_underscores></tag_with_underscores>
Symbols (ie @) in tags
Sometimes you may need to use a tag that uses characters that are not allowed in
function names in python (again, very unusual).
tag_with_AtSymbol()
<tag-with-@symbol></tag-with-@symbol>
Div(normal_arg='normal stuff',**{'notNormal:arg:with_varing@symbols!':'123'})
---
https://docs.fastht.ml/ref/handlers.html
Handling handlers
How handlers work in FastHTML
from fasthtml.common import *
from collections import namedtuple
from typing import TypedDict
from datetime import datetime
import json,time
app = FastHTML()
The FastHTML class is the main application class for FastHTML apps.
rt = app.route
Handler functions can return strings directly. These strings are sent as the
response body to the client.
cli = Client(app)
cli.get('/hi').text
'Hi there'
The get method on a Client instance simulates GET requests to the app. It returns a
response object that has a .text attribute, which you can use to access the body of
the response. It calls httpx.get internally – all httpx HTTP verbs are supported.
@rt("/hi")
def post(): return 'Postal'
cli.post('/hi').text
'Postal'
Handler functions can be defined for different HTTP methods on the same route.
Here, we define a post handler for the /hi route. The Client instance can simulate
different HTTP methods, including POST requests.
'testserver'
Handler functions can accept a req (or request) parameter, which represents the
incoming request. This object contains information about the request, including
headers. In this example, we return the host header from the request. The test
client uses ‘testserver’ as the default host.
@rt
def yoyo(): return 'a yoyo'
cli.post('/yoyo').text
'a yoyo'
If the @rt decorator is used without arguments, it uses the function name as the
route path. Here, the yoyo function becomes the handler for the /yoyo route. This
handler responds to GET and POST methods, since a specific method wasn’t provided.
@rt
def ft1(): return Html(Div('Text.'))
print(cli.get('/ft1').text)
<html>
<div>Text.</div>
</html>
Handler functions can return FT objects, which are automatically converted to HTML
strings. The FT class can take other FT components as arguments, such as Div. This
allows for easy composition of HTML elements in your responses.
@app.get
def autopost(): return Html(Div('Text.', hx_post=yoyo.to()))
print(cli.get('/autopost').text)
<html>
<div hx-post="/yoyo">Text.</div>
</html>
The rt decorator modifies the yoyo function by adding an rt() method. This method
returns the route path associated with the handler. It’s a convenient way to
reference the route of a handler function dynamically.
In the example, yoyo.to() is used as the value for hx_post. This means when the div
is clicked, it will trigger an HTMX POST request to the route of the yoyo handler.
This approach allows for flexible, DRY code by avoiding hardcoded route strings and
automatically updating if the route changes.
@app.get
def autoget(): return Html(Body(Div('Text.', cls='px-2',
hx_post=show_host.to(a='b'))))
print(cli.get('/autoget').text)
<html>
<body>
<div hx-post="/hostie?a=b" class="px-2">Text.</div>
</body>
</html>
The rt() method of handler functions can also accept parameters. When called with
parameters, it returns the route path with a query string appended. In this
example, show_host.to(a='b') generates the path /hostie?a=b.
The cls parameter is used to add a CSS class to the Div. This translates to the
class attribute in the rendered HTML. (class can’t be used as a parameter name
directly in Python since it’s a reserved word.)
@rt('/ft2')
def get(): return Title('Foo'),H1('bar')
print(cli.get('/ft2').text)
<!doctype html>
<html>
<head>
<title>Foo</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-
fit=cover">
<script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script><script
src="https://cdn.jsdelivr.net/gh/answerdotai/[email protected]/fasthtml.js"></
script><script
src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></
script><script
src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></
script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight},
'*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<h1>bar</h1>
</body>
</html>
Handler functions can return multiple FT objects as a tuple. The first item is
treated as the Title, and the rest are added to the Body. When the request is not
an HTMX request, FastHTML automatically adds necessary HTML boilerplate, including
default head content with required scripts.
When using app.route (or rt), if the function name matches an HTTP verb (e.g., get,
post, put, delete), that HTTP method is automatically used for the route. In this
case, a path must be explicitly provided as an argument to the decorator.
hxhdr = {'headers':{'hx-request':"1"}}
print(cli.get('/ft2', **hxhdr).text)
<title>Foo</title>
<h1>bar</h1>
For HTMX requests (indicated by the hx-request header), FastHTML returns only the
specified components without the full HTML structure. This allows for efficient
partial page updates in HTMX applications.
@rt('/ft3')
def get(): return H1('bar')
print(cli.get('/ft3', **hxhdr).text)
<h1>bar</h1>
When a handler function returns a single FT object for an HTMX request, it’s
rendered as a single HTML partial.
@rt('/ft4')
def get(): return Html(Head(Title('hi')), Body(P('there')))
print(cli.get('/ft4').text)
<html>
<head>
<title>hi</title>
</head>
<body>
<p>there</p>
</body>
</html>
Handler functions can return a complete Html structure, including Head and Body
components. When a full HTML structure is returned, FastHTML doesn’t add any
additional boilerplate. This gives you full control over the HTML output when
needed.
@rt
def index(): return "welcome!"
print(cli.get('/').text)
welcome!
The index function is a special handler in FastHTML. When defined without arguments
to the @rt decorator, it automatically becomes the handler for the root path ('/').
This is a convenient way to define the main page or entry point of your
application.
The name parameter in the decorator allows you to give the route a name, which can
be used for URL generation.
In this example, {nm} in the route becomes the nm parameter in the function. The
function uses this parameter to create a personalized greeting.
@app.get
def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))
print(cli.get('/autolink').text)
<html>
<div href="/user/Alexis">Text.</div>
</html>
The uri function is used to generate URLs for named routes. It takes the route name
as its first argument, followed by any path or query parameters needed for that
route.
In this example, uri('gday', nm='Alexis') generates the URL for the route named
‘gday’ (which we defined earlier as ‘/user/{nm}’), with ‘Alexis’ as the value for
the ‘nm’ parameter.
The link parameter in FT components sets the href attribute of the rendered HTML
element. By using uri(), we can dynamically generate correct URLs even if the
underlying route structure changes.
@rt('/link')
def get(req): return f"{req.url_for('gday', nm='Alexis')};
{req.url_for('show_host')}"
cli.get('/link').text
'http://testserver/user/Alexis; http://testserver/hostie'
The url_for method of the request object can be used to generate URLs for named
routes. It takes the route name as its first argument, followed by any path
parameters needed for that route.
In this example, req.url_for('gday', nm='Alexis') generates the full URL for the
route named ‘gday’, including the scheme and host. Similarly,
req.url_for('show_host') generates the URL for the ‘show_host’ route.
This method is particularly useful when you need to generate absolute URLs, such as
for email links or API responses. It ensures that the correct host and scheme are
included, even if the application is accessed through different domains or
protocols.
app.url_path_for('gday', nm='Jeremy')
'/user/Jeremy'
The url_path_for method of the application can be used to generate URL paths for
named routes. Unlike url_for, it returns only the path component of the URL,
without the scheme or host.
In this example, app.url_path_for('gday', nm='Jeremy') generates the path
‘/user/Jeremy’ for the route named ‘gday’.
This method is useful when you need relative URLs or just the path component, such
as for internal links or when constructing URLs in a host-agnostic manner.
@rt('/oops')
def get(nope): return nope
r = cli.get('/oops?nope=1')
print(r)
r.text
When a parameter is ignored, it doesn’t receive the value from the query string.
This can lead to unexpected behavior, as the function attempts to return nope,
which is undefined.
The cli.get('/oops?nope=1') call succeeds with a 200 OK status because the handler
doesn’t raise an exception, but it returns an empty response, rather than the
intended value.
To fix this, you should either add a type annotation to the parameter (e.g., def
get(nope: str):) or use a recognized special name like req.
@rt('/html/{idx}')
def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
print(cli.get('/html/1', **hxhdr).text)
<body>
<h4>Next is 2.</h4>
</body>
Path parameters can be type-annotated, and FastHTML will automatically convert them
to the specified type if possible. In this example, idx is annotated as int, so
it’s converted from the string in the URL to an integer.
reg_re_param("imgext", "ico|gif|jpg|jpeg|webm")
@rt(r'/static/{path:path}{fn}.{ext:imgext}')
def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
print(cli.get('/static/foo/jph.ico').text)
Handler functions can use complex path patterns with multiple parameters and custom
types. In this example, the route pattern r'/static/{path:path}{fn}.{ext:imgext}'
uses three path parameters:
@rt("/models/{nm}")
def get(nm:ModelName): return nm
print(cli.get('/models/alexnet').text)
alexnet
We define ModelName as an enum with three possible values: “alexnet”, “resnet”, and
“lenet”. Handler functions can use these enum types as parameter annotations. In
this example, the nm parameter is annotated with ModelName, which ensures that only
valid model names are accepted.
When a request is made with a valid model name, the handler function returns that
name. This pattern is useful for creating type-safe APIs with a predefined set of
valid values.
@rt("/files/{path}")
async def get(path: Path): return path.with_suffix('.txt')
print(cli.get('/files/foo').text)
foo.txt
Handler functions can use Path objects as parameter types. The Path type is from
Python’s standard library pathlib module, which provides an object-oriented
interface for working with file paths. In this example, the path parameter is
annotated with Path, so FastHTML automatically converts the string from the URL to
a Path object.
@rt("/items/")
def get(idx:int|None = 0): return fake_db[idx]
print(cli.get('/items/?idx=1').text)
{"name":"Bar"}
Handler functions can use query parameters, which are automatically parsed from the
URL. In this example, idx is a query parameter with a default value of 0. It’s
annotated as int|None, allowing it to be either an integer or None.
The function uses this parameter to index into a fake database (fake_db). When a
request is made with a valid idx query parameter, the handler returns the
corresponding item from the database.
print(cli.get('/items/').text)
{"name":"Foo"}
When no idx query parameter is provided, the handler function uses the default
value of 0. This results in returning the first item from the fake_db list, which
is {"name":"Foo"}.
This behavior demonstrates how default values for query parameters work in
FastHTML. They allow the API to have a sensible default behavior when optional
parameters are not provided.
print(cli.get('/items/?idx=g'))
This behavior ensures type safety and prevents invalid inputs from reaching the
handler function.
@app.get("/booly/")
def _(coming:bool=True): return 'Coming' if coming else 'Not coming'
print(cli.get('/booly/?coming=true').text)
print(cli.get('/booly/?coming=no').text)
Coming
Not coming
Handler functions can use boolean query parameters. In this example, coming is a
boolean parameter with a default value of True. FastHTML automatically converts
string values like ‘true’, ‘false’, ‘1’, ‘0’, ‘on’, ‘off’, ‘yes’, and ‘no’ to their
corresponding boolean values.
The underscore _ is used as the function name in this example to indicate that the
function’s name is not important or won’t be referenced elsewhere. This is a common
Python convention for throwaway or unused variables, and it works here because
FastHTML uses the route decorator parameter, when provided, to determine the URL
path, not the function name. By default, both get and post methods can be used in
routes that don’t specify an http method (by either using app.get, def get, or the
methods parameter to app.route).
@app.get("/datie/")
def _(d:parsed_date): return d
date_str = "17th of May, 2024, 2p"
print(cli.get(f'/datie/?d={date_str}').text)
2024-05-17 14:00:00
Handler functions can use date objects as parameter types. FastHTML uses
dateutil.parser library to automatically parse a wide variety of date string
formats into date objects.
@app.get("/ua")
async def _(user_agent:str): return user_agent
print(cli.get('/ua', headers={'User-Agent':'FastHTML'}).text)
FastHTML
Handler functions can access HTTP headers by using parameter names that match the
header names. In this example, user_agent is used as a parameter name, which
automatically captures the value of the ‘User-Agent’ header from the request.
The Client instance allows setting custom headers for test requests. Here, we set
the ‘User-Agent’ header to ‘FastHTML’ in the test request.
@app.get("/hxtest")
def _(htmx): return htmx.request
print(cli.get('/hxtest', headers={'HX-Request':'1'}).text)
@app.get("/hxtest2")
def _(foo:HtmxHeaders, req): return foo.request
print(cli.get('/hxtest2', headers={'HX-Request':'1'}).text)
1
1
Handler functions can access HTMX-specific headers using either the special htmx
parameter name, or a parameter annotated with HtmxHeaders. Both approaches provide
access to HTMX-related information.
In these examples, the htmx.request attribute returns the value of the ‘HX-Request’
header.
app.chk = 'foo'
@app.get("/app")
def _(app): return app.chk
print(cli.get('/app').text)
foo
Handler functions can access the FastHTML application instance using the special
app parameter name. This allows handlers to access application-level attributes and
methods.
In this example, we set a custom attribute chk on the application instance. The
handler function then uses the app parameter to access this attribute and return
its value.
@app.get("/app2")
def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval")
r = cli.get('/app2', **hxhdr)
print(r.text)
print(r.headers)
foo
Headers({'mykey': 'myval', 'content-length': '3', 'content-type': 'text/html;
charset=utf-8'})
Handler functions can access the FastHTML application instance using a parameter
annotated with FastHTML. This allows handlers to access application-level
attributes and methods, just like using the special app parameter name.
Handlers can return tuples containing both content and HttpHeader objects.
HttpHeader allows setting custom HTTP headers in the response.
In this example:
We define a handler that returns both the chk attribute from the application and a
custom header.
The HttpHeader("mykey", "myval") sets a custom header in the response.
We use the test client to make a request and examine both the response text and
headers.
The response includes the custom header “mykey” along with standard headers like
content-length and content-type.
@app.get("/app3")
def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org")
r = cli.get('/app3')
print(r.headers)
@app.get("/app4")
def _(foo:FastHTML): return Redirect("http://example.org")
cli.get('/app4', follow_redirects=False)
In this example:
Redirect.__response__
The __response__ method takes a req parameter, which represents the incoming
request. This allows the method to access request information if needed when
constructing the redirect response.
@rt
def meta():
return ((Title('hi'),H1('hi')),
(Meta(property='image'), Meta(property='site_name')))
print(cli.post('/meta').text)
<!doctype html>
<html>
<head>
<title>hi</title>
<meta property="image">
<meta property="site_name">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-
fit=cover">
<script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script><script
src="https://cdn.jsdelivr.net/gh/answerdotai/[email protected]/fasthtml.js"></
script><script
src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></
script><script
src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></
script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight},
'*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<h1>hi</h1>
</body>
</html>
FastHTML automatically identifies elements typically placed in the <head> (like
Title and Meta) and positions them accordingly, while other elements go in the
<body>.
In this example: - (Title('hi'), H1('hi')) defines the title and main heading. The
title is placed in the head, and the H1 in the body. - (Meta(property='image'),
Meta(property='site_name')) defines two meta tags, which are both placed in the
head.
APIRouter
APIRouter is useful when you want to split your application routes across
multiple .py files that are part of a single FastHTMl application. It accepts an
optional prefix argument that will be applied to all routes within that instance of
APIRouter.
# products.py
ar = APIRouter(prefix="/products")
@ar("/all")
def all_products(req):
return Div(
"Welcome to the Products Page! Click the button below to look at the
details for product 42",
Div(
Button(
"Details",
hx_get=req.url_for("details", pid=42),
hx_target="#products_list",
hx_swap="outerHTML",
),
),
id="products_list",
)
@ar.get("/{pid}", name="details")
def details(pid: int):
return f"Here are the product details for ID: {pid}"
Since we specified the prefix=/products in our hypothetical products.py file, all
routes defined in that file will be found under /products.
print(str(ar.rt_funcs.all_products))
print(str(ar.rt_funcs.details))
/products/all
/products/{pid}
# main.py
# from products import ar
app, rt = fast_app()
ar.to_app(app)
@rt
def index():
return Div(
"Click me for a look at our products",
hx_get=ar.rt_funcs.all_products,
hx_swap="outerHTML",
)
Note how you can reference our python route functions via APIRouter.rt_funcs in
your hx_{http_method} calls like normal.
If required form data is missing, FastHTML automatically returns a 400 Bad Request
response with an error message.
The data parameter in the cli.post() method simulates sending form data in the
request.
@app.post('/pet/dog')
def pet_dog(dogname: str = None): return dogname or 'unknown name'
print(cli.post('/pet/dog', data={}).text)
Here, if the form data doesn’t include the dogname field, the function uses the
default value. The function returns either the provided dogname or ‘unknown name’
if dogname is None.
@dataclass
class Bodie: a:int;b:str
@rt("/bodie/{nm}")
def post(nm:str, data:Bodie):
res = asdict(data)
res['nm'] = nm
return res
FastHTML automatically converts the incoming form data to a Bodie instance where
attribute names match parameter names. Other form data elements are matched with
parameters with the same names (in this case, nm).
@app.post("/bodied/")
def bodied(data:dict): return data
d = dict(a=1, b='foo')
print(cli.post('/bodied/', data=d).text)
Note that when form data is converted to a dictionary, all values become strings,
even if they were originally numbers. This is why the ‘a’ key in the response has a
string value “1” instead of the integer 1.
nt = namedtuple('Bodient', ['a','b'])
@app.post("/bodient/")
def bodient(data:nt): return asdict(data)
print(cli.post('/bodient/', data=d).text)
FastHTML automatically converts the incoming form data to a Bodient instance where
field names match parameter names. As with the previous example, all form data
values are converted to strings in the process.
@app.post("/bodietd/")
def bodient(data:BodieTD): return data
print(cli.post('/bodietd/', data=d).text)
FastHTML automatically converts the incoming form data to a BodieTD instance where
keys match the defined fields. Unlike with regular dictionaries or named tuples,
FastHTML respects the type hints in TypedDict, converting values to the specified
types when possible (e.g., converting ‘1’ to the integer 1 for the ‘a’ field).
class Bodie2:
a:int|None; b:str
def __init__(self, a, b='foo'): store_attr()
@app.post("/bodie2/")
def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}"
print(cli.post('/bodie2/', data={'a':1}).text)
@app.post("/b")
def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))
The Titled component is used to create a page with a title and main content. It
automatically generates an <h1> with the provided title, wraps the content in a
<main> tag with a “container” class, and adds a title to the head.
When making a request with JSON data: - Set the “Content-Type” header to
“application/json” - Provide the JSON data as a string in the data parameter of the
request
@rt("/getcookie")
def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
print(cli.get('/setcookie').text)
time.sleep(0.01)
cli.get('/getcookie').text
The /setcookie route sets a cookie named ‘now’ with the current datetime.
The /getcookie route retrieves the ‘now’ cookie and returns its value.
The cookie() function is used to create a cookie response. FastHTML automatically
converts the datetime object to a string when setting the cookie, and parses it
back to a date object when retrieving it.
cookie('now', datetime.now())
app = FastHTML(secret_key='soopersecret')
cli = Client(app)
rt = app.route
@rt("/setsess")
def get(sess, foo:str=''):
now = datetime.now()
sess['auth'] = str(now)
return f'Set to {now}'
@rt("/getsess")
def get(sess): return f'Session time: {sess["auth"]}'
print(cli.get('/setsess').text)
time.sleep(0.01)
cli.get('/getsess').text
The sess parameter in handler functions provides access to the session data. You
can set and get session variables using dictionary-style access.
@rt("/upload")
async def post(uf:UploadFile): return (await uf.read()).decode()
# Release notes
Handler functions can accept file uploads using Starlette’s UploadFile type. In
this example:
app.static_route('.md', static_path='../..')
print(cli.get('/README.md').text[:10])
# FastHTML
The static_route method of the FastHTML application allows serving static files
with specified extensions from a given directory. In this example:
.md files are served from the ../.. directory (two levels up from the current
directory).
Accessing /README.md returns the contents of the README.md file from that
directory.
help(app.static_route_exts)
reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|
eot|woff2|txt|html|map")
@rt("/form-submit/{list_id}")
def options(list_id: str):
headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': '*',
}
return Response(status_code=200, headers=headers)
print(cli.options('/form-submit/2').headers)
app = FastHTML(exception_handlers={404:_not_found})
cli = Client(app)
rt = app.route
r = cli.get('/')
print(r.text)
<!doctype html>
<html>
<head>
<title>FastHTML page</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-
fit=cover">
<script src="https://unpkg.com/htmx.org@next/dist/htmx.min.js"></script><script
src="https://cdn.jsdelivr.net/gh/answerdotai/[email protected]/fasthtml.js"></
script><script
src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></
script><script
src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></
script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight},
'*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div>nope</div>
</body>
</html>
FastHTML allows you to define custom exception handlers – in this case, a custom
404 (Not Found) handler function _not_found, which returns a Div component with the
text ‘nope’.
---
https://docs.fastht.ml/ref/live_reload.html
Live Reloading
When building your app it can be useful to view your changes in a web browser as
you make them. FastHTML supports live reloading which means that it watches for any
changes to your code and automatically refreshes the webpage in your browser.
main.py
from fasthtml.common import *
1app, rt = fast_app(live=True)
2serve()
1
fast_app() instantiates the FastHTMLWithLiveReload class.
2
serve() is a wrapper around a uvicorn call.
To run main.py in live reload mode, just do python main.py. We recommend turning
off live reload when deploying your app to production.
---
https://docs.fastht.ml/api/core.html
Core
The FastHTML subclass of Starlette, along with the RouterX and RouteX classes it
automatically uses.
This is the source code to fasthtml. You won’t need to read this unless you want to
understand how things are built behind the scenes, or need full details of a
particular API. The notebook is converted to the Python module fasthtml/core.py
using nbdev.
We write source code first, and then tests come after. The tests serve as both a
means to confirm that the code works and also serves as working examples. The first
exported function, parsed_date, is an example of this pattern.
source
parsed_date
parsed_date (s:str)
Convert s to a datetime
parsed_date('2pm')
True
source
snake2hyphens
snake2hyphens (s:str)
Convert s from snake case to hyphenated and capitalised
snake2hyphens("snake_case")
'Snake-Case'
source
HtmxHeaders
HtmxHeaders (boosted:str|None=None, current_url:str|None=None,
history_restore_request:str|None=None, prompt:str|None=None,
request:str|None=None, target:str|None=None,
trigger_name:str|None=None, trigger:str|None=None)
def test_request(url: str='/', headers: dict={}, method: str='get') -> Request:
scope = {
'type': 'http',
'method': method,
'path': url,
'headers': Headers(headers).raw,
'query_string': b'',
'scheme': 'http',
'client': ('127.0.0.1', 8000),
'server': ('127.0.0.1', 8000),
}
receive = lambda: {"body": b"", "more_body": False}
return Request(scope, receive)
h = test_request(headers=Headers({'HX-Request':'1'}))
_get_htmx(h.headers)
d = dict(k=int, l=List[int])
test_eq(_form_arg('k', "1", d), 1)
test_eq(_form_arg('l', "1", d), [1])
test_eq(_form_arg('l', ["1","2"], d), [1,2])
source
HttpHeader
HttpHeader (k:str, v:str)
_to_htmx_header('trigger_after_settle')
'HX-Trigger-After-Settle'
source
HtmxResponseHeaders
HtmxResponseHeaders (location=None, push_url=None, redirect=None,
refresh=None, replace_url=None, reswap=None,
retarget=None, reselect=None, trigger=None,
trigger_after_settle=None, trigger_after_swap=None)
HTMX response headers
HtmxResponseHeaders(trigger_after_settle='hi')
HttpHeader(k='HX-Trigger-After-Settle', v='hi')
source
form2dict
form2dict (form:starlette.datastructures.FormData)
Convert starlette form data to a dict
d = [('a',1),('a',2),('b',0)]
fd = FormData(d)
res = form2dict(fd)
test_eq(res['a'], [1,2])
test_eq(res['b'], 0)
source
parse_form
parse_form (req:starlette.requests.Request)
Starlette errors on empty multipart forms, so this checks for that situation
d = dict(k='value1',v=['value2','value3'])
response = client.post('/', data=d)
print(response.json())
"['1', '2']"
def g(req, this:Starlette, a:str, b:HttpHeader): ...
flat_xt
flat_xt (lst)
Flatten lists
x = ft('a',1)
test_eq(flat_xt([x, x, [x,x]]), (x,)*4)
test_eq(flat_xt(x), (x,))
source
Beforeware
Beforeware (f, skip=None)
Initialize self. See help(type(self)) for accurate signature.
Websockets / SSE
def on_receive(self, msg:str): return f"Message text was: {msg}"
c = _ws_endp(on_receive)
cli = TestClient(Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))]))
with cli.websocket_connect('/') as ws:
ws.send_text('{"msg":"Hi!"}')
data = ws.receive_text()
assert data == 'Message text was: Hi!'
source
EventStream
EventStream (s)
Create a text/event-stream response from s
source
signal_shutdown
signal_shutdown ()
Routing and application
source
uri
uri (_arg, **kwargs)
source
decode_uri
decode_uri (s)
source
StringConvertor.to_string
StringConvertor.to_string (value:str)
source
HTTPConnection.url_path_for
HTTPConnection.url_path_for (name:str, **path_params)
source
flat_tuple
flat_tuple (o)
Flatten lists
source
noop_body
noop_body (c, req)
Default Body wrap function which just returns the content
source
respond
respond (req, heads, bdy)
Default FT response creation function
source
Redirect
Redirect (loc)
Use HTMX or Starlette RedirectResponse as required to redirect to loc
source
get_key
get_key (key=None, fname='.sesskey')
get_key()
'5a5e5544-5ee8-46f2-836e-924976ce8b58'
source
qp
qp (p:str, **kw)
Add query parameters to path p
'/foo?a=&b=&c=1&c=2&d=bar'
source
def_hdrs
def_hdrs (htmx=True, surreal=True)
Default headers for a FastHTML app
source
FastHTML
FastHTML (debug=False, routes=None, middleware=None, title:str='FastHTML
page', exception_handlers=None, on_startup=None,
on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,
exts=None, before=None, after=None, surreal=True, htmx=True,
default_hdrs=True, sess_cls=<class
'starlette.middleware.sessions.SessionMiddleware'>,
secret_key=None, session_cookie='session_', max_age=31536000,
sess_path='/', same_site='lax', sess_https_only=False,
sess_domain=None, key_fname='.sesskey', body_wrap=<function
noop_body>, htmlkw=None, nb_hdrs=False, **bodykw)
Creates an Starlette application.
source
FastHTML.ws
FastHTML.ws (path:str, conn=None, disconn=None, name=None,
middleware=None)
Add a websocket route at path
source
nested_name
nested_name (f)
*Get name of function f using ’_’ to join nested function names*
def f():
def g(): ...
return g
func = f()
nested_name(func)
'f_g'
source
FastHTML.route
FastHTML.route (path:str=None, methods=None, name=None,
include_in_schema=True, body_wrap=None)
Add a route at path
app = FastHTML()
@app.get
def foo(a:str, b:list[int]): ...
print(app.routes)
foo.to(a='bar', b=[1,2])
serve
serve (appname=None, app='app', host='0.0.0.0', port=None, reload=True,
reload_includes:list[str]|str|None=None,
reload_excludes:list[str]|str|None=None)
Run the app in an async server, with live reload set as the default.
Client
Client (app, url='http://testserver')
A simple httpx ASGI client that doesn’t require async
cli.get('/').text
'test'
Note that you can also use Starlette’s TestClient instead of FastHTML’s Client.
They should be largely interchangable.
FastHTML Tests
def get_cli(app): return app,TestClient(app),app.route
app,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))
print(app.routes)
response = cli.get('/foo')
assert '<title>My Custom Title</title>' in response.text
foo.to(param='value')
@rt('/xt2')
def get(): return H1('bar')
txt = cli.get('/xt2').text
assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>'
in txt
@rt("/hi")
def get(): return 'Hi there'
r = cli.get('/hi')
r.text
'Hi there'
@rt("/hi")
def post(): return 'Postal'
cli.post('/hi').text
'Postal'
@app.get("/hostie")
def show_host(req): return req.headers['host']
cli.get('/hostie').text
'testserver'
@app.get("/setsess")
def set_sess(session):
session['foo'] = 'bar'
return 'ok'
@app.ws("/ws")
def ws(self, msg:str, ws:WebSocket, session): return f"Message text was: {msg} with
session {session.get('foo')}, from client: {ws.client}"
cli.get('/setsess')
with cli.websocket_connect('/ws') as ws:
ws.send_text('{"msg":"Hi!"}')
data = ws.receive_text()
assert 'Message text was: Hi! with session bar' in data
print(data)
Message text was: Hi! with session bar, from client: Address(host='testclient',
port=50000)
@rt
def yoyo(): return 'a yoyo'
cli.post('/yoyo').text
'a yoyo'
@app.get
def autopost(): return Html(Div('Text.', hx_post=yoyo()))
print(cli.get('/autopost').text)
<!doctype html>
<html>
<div hx-post="a yoyo">Text.</div>
</html>
@app.get
def autopost2(): return Html(Body(Div('Text.', cls='px-2',
hx_post=show_host.to(a='b'))))
print(cli.get('/autopost2').text)
<!doctype html>
<html>
<body>
<div class="px-2" hx-post="/hostie?a=b">Text.</div>
</body>
</html>
@app.get
def autoget2(): return Html(Div('Text.', hx_get=show_host))
print(cli.get('/autoget2').text)
<!doctype html>
<html>
<div hx-get="/hostie">Text.</div>
</html>
@rt('/user/{nm}', name='gday')
def get(nm:str=''): return f"Good day to you, {nm}!"
cli.get('/user/Alexis').text
<!doctype html>
<html>
<div href="/user/Alexis">Text.</div>
</html>
@rt('/link')
def get(req): return f"{req.url_for('gday', nm='Alexis')};
{req.url_for('show_host')}"
cli.get('/link').text
'http://testserver/user/Alexis; http://testserver/hostie'
@app.get("/background")
async def background_task(request):
async def long_running_task():
await asyncio.sleep(0.1)
print("Background task completed!")
return P("Task started"), BackgroundTask(long_running_task)
response = cli.get("/background")
hxhdr = {'headers':{'hx-request':"1"}}
@rt('/ft')
def get(): return Title('Foo'),H1('bar')
txt = cli.get('/ft').text
assert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
@rt('/xt2')
def get(): return H1('bar')
txt = cli.get('/xt2').text
assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>'
in txt
@rt('/xt3')
def get(): return Html(Head(Title('hi')), Body(P('there')))
txt = cli.get('/xt3').text
assert '<title>FastHTML page</title>' not in txt and '<title>hi</title>' in txt and
'<p>there</p>' in txt
@rt('/oops')
def get(nope): return nope
test_warns(lambda: cli.get('/oops?nope=1'))
@rt('/html/{idx}')
async def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
@rt("/models/{nm}")
def get(nm:ModelName): return nm
@rt("/files/{path}")
async def get(path: Path): return path.with_suffix('.txt')
@rt("/items/")
def get(idx:int|None = 0): return fake_db[idx]
@rt("/idxl/")
def get(idx:list[int]): return str(idx)
r = cli.get('/html/1', headers={'hx-request':"1"})
assert '<h4>Next is 2.</h4>' in r.text
test_r(cli, '/models/alexnet', 'alexnet')
test_r(cli, '/files/foo', 'foo.txt')
test_r(cli, '/items/?idx=1', '{"name":"Bar"}')
test_r(cli, '/items/', '{"name":"Foo"}')
assert cli.get('/items/?idx=g').text=='404 Not Found'
assert cli.get('/items/?idx=g').status_code == 404
test_r(cli, '/idxl/?idx=1&idx=2', '[1, 2]')
assert cli.get('/idxl/?idx=1&idx=g').status_code == 404
app = FastHTML()
rt = app.route
cli = TestClient(app)
@app.route(r'/static/{path:path}.jpg')
def index(path:str): return f'got {path}'
cli.get('/static/sub/a.b.jpg').text
'got sub/a.b'
app.chk = 'foo'
@app.get("/booly/")
def _(coming:bool=True): return 'Coming' if coming else 'Not coming'
@app.get("/datie/")
def _(d:parsed_date): return d
@app.get("/ua")
async def _(user_agent:str): return user_agent
@app.get("/hxtest")
def _(htmx): return htmx.request
@app.get("/hxtest2")
def _(foo:HtmxHeaders, req): return foo.request
@app.get("/app")
def _(app): return app.chk
@app.get("/app2")
def _(foo:FastHTML): return foo.chk,HttpHeader("mykey", "myval")
@app.get("/app3")
def _(foo:FastHTML): return HtmxResponseHeaders(location="http://example.org")
@app.get("/app4")
def _(foo:FastHTML): return Redirect("http://example.org")
r = cli.get('/app2', **hxhdr)
test_eq(r.text, 'foo')
test_eq(r.headers['mykey'], 'myval')
r = cli.get('/app3')
test_eq(r.headers['HX-Location'], 'http://example.org')
r = cli.get('/app4', follow_redirects=False)
test_eq(r.status_code, 303)
r = cli.get('/app4', headers={'HX-Request':'1'})
test_eq(r.headers['HX-Redirect'], 'http://example.org')
@rt
def meta():
return ((Title('hi'),H1('hi')),
(Meta(property='image'), Meta(property='site_name'))
)
t = cli.post('/meta').text
assert re.search(r'<body>\s*<h1>hi</h1>\s*</body>', t)
assert '<meta' in t
@app.post('/profile/me')
def profile_update(username: str): return username
@rt("/bodie/{nm}")
def post(nm:str, data:Bodie):
res = asdict(data)
res['nm'] = nm
return res
@app.post("/bodied/")
def bodied(data:dict): return data
nt = namedtuple('Bodient', ['a','b'])
@app.post("/bodient/")
def bodient(data:nt): return asdict(data)
@app.post("/bodietd/")
def bodient(data:BodieTD): return data
class Bodie2:
a:int|None; b:str
def __init__(self, a, b='foo'): store_attr()
@rt("/bodie2/", methods=['get','post'])
def bodie(d:Bodie2): return f"a: {d.a}; b: {d.b}"
d = dict(a=1, b='foo')
@rt("/uploads")
async def post(files:list[UploadFile]):
return ','.join([(await file.read()).decode() for file in files])
200
content1,content2
res = cli.post('/uploads', files=[files[0]])
print(res.status_code)
print(res.text)
200
content1
@rt("/setsess")
def get(sess, foo:str=''):
now = datetime.now()
sess['auth'] = str(now)
return f'Set to {now}'
@rt("/getsess")
def get(sess): return f'Session time: {sess["auth"]}'
print(cli.get('/setsess').text)
time.sleep(0.01)
cli.get('/getsess').text
@rt("/getsess-all")
def get(sess): return sess['name']
test_eq(cli.get('/getsess-all').text, '2')
@rt("/upload")
async def post(uf:UploadFile): return (await uf.read()).decode()
# Release notes
@rt("/form-submit/{list_id}")
def options(list_id: str):
headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': '*',
}
return Response(status_code=200, headers=headers)
h = cli.options('/form-submit/2').headers
test_eq(h['Access-Control-Allow-Methods'], 'POST')
app,cli,rt = get_cli(FastHTML(exception_handlers={404:_not_found}))
txt = cli.get('/').text
assert '<div>nope</div>' in txt
assert '<!doctype html>' in txt
app,cli,rt = get_cli(FastHTML())
@rt("/{name}/{age}")
def get(name: str, age: int):
return Titled(f"Hello {name.title()}, age {age}")
auth = user_pwd_auth(testuser='spycraft')
app,cli,rt = get_cli(FastHTML(middleware=[auth]))
@rt("/locked")
def get(auth): return 'Hello, ' + auth
auth = user_pwd_auth(testuser='spycraft')
app,cli,rt = get_cli(FastHTML(middleware=[auth]))
@rt("/locked")
def get(auth): return 'Hello, ' + auth
APIRouter
source
RouteFuncs
RouteFuncs ()
Initialize self. See help(type(self)) for accurate signature.
source
APIRouter
APIRouter (prefix:str|None=None, body_wrap=<function noop_body>)
Add routes to an app
ar = APIRouter()
@ar("/hi")
def get(): return 'Hi there'
@ar("/hi")
def post(): return 'Postal'
@ar
def ho(): return 'Ho ho'
@ar("/hostie")
def show_host(req): return req.headers['host']
@ar
def yoyo(): return 'a yoyo'
@ar
def index(): return "home page"
@ar.ws("/ws")
def ws(self, msg:str): return f"Message text was: {msg}"
app,cli,_ = get_cli(FastHTML())
ar.to_app(app)
ar2 = APIRouter("/products")
@ar2("/hi")
def get(): return 'Hi there'
@ar2("/hi")
def post(): return 'Postal'
@ar2
def ho(): return 'Ho ho'
@ar2("/hostie")
def show_host(req): return req.headers['host']
@ar2
def yoyo(): return 'a yoyo'
@ar2
def index(): return "home page"
@ar2.ws("/ws")
def ws(self, msg:str): return f"Message text was: {msg}"
app,cli,_ = get_cli(FastHTML())
ar2.to_app(app)
@ar.get
def hi2(): return 'Hi there'
@ar.get("/hi3")
def _(): return 'Hi there'
@ar.post("/post2")
def _(): return 'Postal'
@ar2.get
def hi2(): return 'Hi there'
@ar2.get("/hi3")
def _(): return 'Hi there'
@ar2.post("/post2")
def _(): return 'Postal'
Extras
app,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))
source
cookie
cookie (key:str, value='', max_age=None, expires=None, path='/',
domain=None, secure=False, httponly=False, samesite='lax')
Create a ‘set-cookie’ HttpHeader
@rt("/setcookie")
def get(req): return cookie('now', datetime.now())
@rt("/getcookie")
def get(now:parsed_date): return f'Cookie was set at time {now.time()}'
print(cli.get('/setcookie').text)
time.sleep(0.01)
cli.get('/getcookie').text
reg_re_param
reg_re_param (m, s)
source
FastHTML.static_route_exts
FastHTML.static_route_exts (prefix='/', static_path='.', exts='static')
Add a static route at URL path prefix with files from static_path and exts defined
by reg_re_param()
reg_re_param("imgext", "ico|gif|jpg|jpeg|webm|pdf")
@rt(r'/static/{path:path}{fn}.{ext:imgext}')
def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
app.static_route_exts()
assert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text
source
FastHTML.static_route
FastHTML.static_route (ext='', prefix='/', static_path='.')
Add a static route at URL path prefix with files from static_path and single ext
(including the ‘.’)
app.static_route('.md', static_path='../..')
assert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text
source
MiddlewareBase
MiddlewareBase ()
Initialize self. See help(type(self)) for accurate signature.
source
FtResponse
FtResponse (content, status_code:int=200, headers=None, cls=<class
'starlette.responses.HTMLResponse'>,
media_type:str|None=None)
Wrap an FT response with any Starlette Response
@rt('/ftr')
def get():
cts = Title('Foo'),H1('bar')
return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'})
r = cli.get('/ftr')
test_eq(r.status_code, 201)
test_eq(r.headers['location'], '/foo/1')
txt = r.text
assert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
source
unqid
unqid ()
source
setup_ws
setup_ws (app, f=<function noop>)
---
https://docs.fastht.ml/api/components.html
Components
ft_html and ft_hx functions to add some conveniences to ft, along with a full set
of basic HTML components, and functions to work with forms and FT conversion
from lxml import html as lx
from pprint import pprint
show
show (ft, *rest)
Renders FT Components into HTML within a Jupyter notebook.
When placed within the show() function, this will render the HTML in Jupyter
notebooks.
show(sentence)
FastHTML is Fast
sentence
<p id="sentence_id">
<strong>FastHTML is <i>Fast</i></strong></p>
print(repr(sentence))
FT.__str__
FT.__str__ ()
Return str(self).
If they have an id, then that id is used as the component’s str representation:
f'hx_target=#{sentence}'
'hx_target=#sentence_id'
source
FT.__radd__
FT.__radd__ (b)
'hx_target=#' + sentence
'hx_target=#sentence_id'
source
FT.__add__
FT.__add__ (b)
sentence + '...'
'sentence_id...'
fh_html and fh_hx
source
attrmap_x
attrmap_x (o)
source
ft_html
ft_html (tag:str, *c, id=None, cls=None, title=None, style=None,
attrmap=None, valmap=None, ft_cls=None, **kwargs)
ft_html('a', **{'@click.away':1})
<a @click.away="1"></a>
ft_html('a', {'@click.away':1})
<a @click.away="1"></a>
c = Div(id='someid')
ft_html('a', id=c)
source
ft_hx
ft_hx (tag:str, *c, target_id=None, hx_vals=None, hx_target=None,
id=None, cls=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None, enterkeyhint=None,
hidden=None, inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
hx_select=None, hx_select_oob=None, hx_indicator=None,
hx_push_url=None, hx_confirm=None, hx_disable=None,
hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
hx_headers=None, hx_history=None, hx_history_elt=None,
hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
ft_hx('a', hx_vals={'a':1})
ft_hx('a', hx_target=c)
<a hx-target="#someid"></a>
For tags that have a name attribute, it will be set to the value of id if not
provided explicitly:
Form(Button(target_id='foo', id='btn'),
hx_post='/', target_id='tgt', id='frm')
source
File
File (fname)
Use the unescaped text in file fname directly
a = Input(name='nm')
a
<input name="nm">
a(hx_swap_oob='true')
fill_form
fill_form (form:fastcore.xml.FT, obj)
Fills named items in form using attributes in obj
@dataclass
class TodoItem:
title:str; id:int; done:bool; details:str; opt:str='a'
@dataclass
class MultiSelect:
items: list[str]
@dataclass
class MultiCheck:
items: list[str]
source
fill_dataclass
fill_dataclass (src, dest)
Modifies dataclass in-place and returns it
elem = lx.fromstring(to_xml(form))
test_eq(elem.xpath("//input[@id='title']/@value"), ['Profit'])
source
getattr
__getattr__ (tag)
html2ft
source
html2ft
html2ft (html, attr1st=False)
Convert HTML to an ft expression
h = to_xml(form)
hl_md(html2ft(h), 'python')
Form(
Fieldset(
Input(value='Profit', id='title', name='title', cls='char'),
Label(
Input(type='checkbox', name='done', data_foo='bar', checked='1',
cls='checkboxer'),
'Done',
cls='px-2'
),
Input(type='hidden', id='id', name='id', value='2'),
Select(
Option(value='a'),
Option(value='b', selected='1'),
name='opt'
),
Textarea('Details', id='details', name='details'),
Button('Save'),
name='stuff'
)
)
Form(
Fieldset(name='stuff')(
Input(value='Profit', id='title', name='title', cls='char')(),
Label(cls='px-2')(
Input(type='checkbox', name='done', data_foo='bar', checked='1',
cls='checkboxer')(),
'Done'
),
Input(type='hidden', id='id', name='id', value='2')(),
Select(name='opt')(
Option(value='a')(),
Option(value='b', selected='1')()
),
Textarea(id='details', name='details')('Details'),
Button()('Save')
)
)
source
sse_message
sse_message (elm, event='message')
Convert element elm into a format suitable for SSE streaming
print(sse_message(Div(P('hi'), P('there'))))
event: message
data: <div>
data: <p>hi</p>
data: <p>there</p>
data: </div>
Tests
test_html2ft('<input value="Profit" name="title" id="title" class="char">',
attr1st=True)
test_html2ft('<input value="Profit" name="title" id="title" class="char">')
test_html2ft('<div id="foo"></div>')
test_html2ft('<div id="foo">hi</div>')
test_html2ft('<div x-show="open" x-transition:enter="transition duration-300" x-
transition:enter-start="opacity-0 scale-90">Hello 👋</div>')
test_html2ft('<div x-transition:enter.scale.80
x-transition:leave.scale.90>hello</div>')
---
https://docs.fastht.ml/api/xtend.html
Component extensions
Simple extensions to standard HTML components, such as adding sensible defaults
from pprint import pprint
source
A
A (*c, hx_get=None, target_id=None, hx_swap=None, href='#', hx_vals=None,
hx_target=None, id=None, cls=None, title=None, style=None,
accesskey=None, contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None,
popover=None, spellcheck=None, tabindex=None, translate=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None,
hx_history_elt=None, hx_inherit=None, hx_params=None,
hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,
hx_validate=None, **kwargs)
An A tag; href defaults to ‘#’ for more concise use with HTMX
source
AX
AX (txt, hx_get=None, target_id=None, hx_swap=None, href='#',
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap_oob=None,
hx_include=None, hx_select=None, hx_select_oob=None,
hx_indicator=None, hx_push_url=None, hx_confirm=None,
hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None,
hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be
positional params
Forms
source
Form
Form (*c, enctype='multipart/form-data', target_id=None, hx_vals=None,
hx_target=None, id=None, cls=None, title=None, style=None,
accesskey=None, contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
hx_sync=None, hx_validate=None, **kwargs)
A Form tag; identical to plain ft_hx version except default
enctype='multipart/form-data'
source
Hidden
Hidden (value:Any='', id:Any=None, target_id=None, hx_vals=None,
hx_target=None, cls=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
An Input of type ‘hidden’
source
CheckboxX
CheckboxX (checked:bool=False, label=None, value='1', id=None, name=None,
target_id=None, hx_vals=None, hx_target=None, cls=None,
title=None, style=None, accesskey=None, contenteditable=None,
dir=None, draggable=None, enterkeyhint=None, hidden=None,
inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None,
hx_include=None, hx_select=None, hx_select_oob=None,
hx_indicator=None, hx_push_url=None, hx_confirm=None,
hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
hx_ext=None, hx_headers=None, hx_history=None,
hx_history_elt=None, hx_inherit=None, hx_params=None,
hx_preserve=None, hx_prompt=None, hx_request=None,
hx_sync=None, hx_validate=None, **kwargs)
A Checkbox optionally inside a Label, preceded by a Hidden with matching name
Check me out!
source
Script
Script (code:str='', id=None, cls=None, title=None, style=None,
attrmap=None, valmap=None, ft_cls=None, **kwargs)
A Script tag that doesn’t escape its code
source
Style
Style (*c, id=None, cls=None, title=None, style=None, attrmap=None,
valmap=None, ft_cls=None, **kwargs)
A Style tag that doesn’t escape its code
double_braces
double_braces (s)
Convert single braces to double braces if next to special chars or newline
source
undouble_braces
undouble_braces (s)
Convert double braces to single braces if next to special chars or newline
source
loose_format
loose_format (s, **kw)
String format s using kw, without being strict about braces outside of template
params
source
ScriptX
ScriptX (fname, src=None, nomodule=None, type=None, _async=None,
defer=None, charset=None, crossorigin=None, integrity=None,
**kw)
A script element with contents read from fname
source
replace_css_vars
replace_css_vars (css, pre='tpl', **kwargs)
Replace var(--) CSS variables with kwargs if name prefix matches pre
source
StyleX
StyleX (fname, **kw)
A style element with contents read from fname and variables replaced from kw
source
Nbsp
Nbsp ()
A non-breaking space
Surreal and JS
source
Surreal
Surreal (code:str)
Wrap code in domReadyExecute and set m=me() and p=me('-')
source
On
On (code:str, event:str='click', sel:str='', me=True)
An async surreal.js script block event handler for event on selector sel,p, making
available parent p, event ev, and target e
source
Prev
Prev (code:str, event:str='click')
An async surreal.js script block event handler for event on previous sibling, with
same vars as On
source
Now
Now (code:str, sel:str='')
An async surreal.js script block on selector me(sel)
source
AnyNow
AnyNow (sel:str, code:str)
An async surreal.js script block on selector any(sel)
source
run_js
run_js (js, id=None, **kw)
Run js script, auto-generating id based on name of caller if needed, and js-
escaping any kw params
source
HtmxOn
HtmxOn (eventname:str, code:str)
source
jsd
jsd (org, repo, root, path, prov='gh', typ='script', ver=None, esm=False,
**kwargs)
jsdelivr Script or CSS Link tag, or URL
Other helpers
source
Titled
Titled (title:str='FastHTML app', *args, cls='container', target_id=None,
hx_vals=None, hx_target=None, id=None, style=None,
accesskey=None, contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
An HTML partial containing a Title, and H1, and any provided children
source
Socials
Socials (title, site_name, description, image, url=None, w=1200, h=630,
twitter_site=None, creator=None, card='summary')
OG and Twitter social card headers
source
Favicon
Favicon (light_icon, dark_icon)
Light and dark favicon headers
source
clear
clear (id)
source
with_sid
with_sid (app, dest, path='/')
---
https://docs.fastht.ml/api/js.html
Javascript examples
Basic external Javascript lib wrappers
To expedite fast development, FastHTML comes with several built-in Javascript and
formatting components. These are largely provided to demonstrate FastHTML JS
patterns. There’s far too many JS libs for FastHTML to wrap them all, and as shown
here the code to add FastHTML support is very simple anyway.
source
light_media
light_media (css:str)
Render light media for day mode views
Type Details
css str CSS to be included in the light media query
light_media('.body {color: green;}')
source
dark_media
dark_media (css:str)
Render dark media for night mode views
Type Details
css str CSS to be included in the dark media query
dark_media('.body {color: white;}')
source
MarkdownJS
MarkdownJS (sel='.marked')
Implements browser-based markdown rendering.
__file__ = '../../fasthtml/katex.js'
source
KatexMarkdownJS
KatexMarkdownJS (sel='.marked', inline_delim='$', display_delim='$$',
math_envs=None)
Type Default Details
sel str .marked CSS selector for markdown elements
inline_delim str $ Delimiter for inline math
display_delim str $$ Delimiter for long math
math_envs NoneType None List of environments to render as display math
KatexMarkdown usage example:
longexample = r"""
Long example:
$$\begin{array}{c}
\end{array}$$
"""
app, rt = fast_app(hdrs=[KatexMarkdownJS()])
@rt('/')
def get():
return Titled("Katex Examples",
# Assigning 'marked' class to components renders content as markdown
P(cls='marked')("Inline example: $\sqrt{3x-1}+(1+x)^2$"),
Div(cls='marked')(longexample)
)
source
HighlightJS
HighlightJS (sel='pre code:not([data-highlighted="yes"])',
langs:str|list|tuple='python', light='atom-one-light',
dark='atom-one-dark')
Implements browser-based syntax highlighting. Usage example here.
SortableJS
SortableJS (sel='.sortable', ghost_class='blue-background-class')
Type Default Details
sel str .sortable CSS selector for sortable elements
ghost_class str blue-background-class When an element is being dragged, this is
the class used to distinguish it from the rest
source
MermaidJS
MermaidJS (sel='.language-mermaid', theme='base')
Implements browser-based Mermaid diagram rendering.
```mermaid
graph TD
A --> B
B --> C
C --> E
```
---
https://docs.fastht.ml/api/pico.html
Pico.css components
Basic components for generating Pico CSS tags
picocondlink is the class-conditional css link tag, and picolink is the regular
tag.
show(picocondlink)
source
set_pico_cls
set_pico_cls ()
Run this to make jupyter outputs styled with pico:
set_pico_cls()
source
Card
Card (*c, header=None, footer=None, target_id=None, hx_vals=None,
hx_target=None, id=None, cls=None, title=None, style=None,
accesskey=None, contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
hx_sync=None, hx_validate=None, **kwargs)
A PicoCSS Card, implemented as an Article with optional Header and Footer
head
body
foot
source
Group
Group (*c, target_id=None, hx_vals=None, hx_target=None, id=None,
cls=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None, enterkeyhint=None,
hidden=None, inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
hx_select=None, hx_select_oob=None, hx_indicator=None,
hx_push_url=None, hx_confirm=None, hx_disable=None,
hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
hx_headers=None, hx_history=None, hx_history_elt=None,
hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
A PicoCSS Group, implemented as a Fieldset with role ‘group’
show(Group(Input(), Button("Save")))
source
Search
Search (*c, target_id=None, hx_vals=None, hx_target=None, id=None,
cls=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
A PicoCSS Search, implemented as a Form with role ‘search’
show(Search(Input(type="search"), Button("Search")))
source
Grid
Grid (*c, cls='grid', target_id=None, hx_vals=None, hx_target=None,
id=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None, enterkeyhint=None,
hidden=None, inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
hx_select=None, hx_select_oob=None, hx_indicator=None,
hx_push_url=None, hx_confirm=None, hx_disable=None,
hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
hx_headers=None, hx_history=None, hx_history_elt=None,
hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
A PicoCSS Grid, implemented as child Divs in a Div with class ‘grid’
source
DialogX
DialogX (*c, open=None, header=None, footer=None, id=None,
target_id=None, hx_vals=None, hx_target=None, cls=None,
title=None, style=None, accesskey=None, contenteditable=None,
dir=None, draggable=None, enterkeyhint=None, hidden=None,
inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None,
hx_include=None, hx_select=None, hx_select_oob=None,
hx_indicator=None, hx_push_url=None, hx_confirm=None,
hx_disable=None, hx_replace_url=None, hx_disabled_elt=None,
hx_ext=None, hx_headers=None, hx_history=None,
hx_history_elt=None, hx_inherit=None, hx_params=None,
hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None,
hx_validate=None, **kwargs)
A PicoCSS Dialog, with children inside a Card
source
Container
Container (*args, target_id=None, hx_vals=None, hx_target=None, id=None,
cls=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None, **kwargs)
A PicoCSS Container, implemented as a Main with class ‘container’
source
PicoBusy
PicoBusy ()
---
https://docs.fastht.ml/api/svg.html
SVG
Simple SVG FT elements
from nbdev.showdoc import show_doc
You can create SVGs directly from strings, for instance (as always, use NotStr or
Safe to tell FastHTML to not escape the text):
To create and modify SVGs using a Python API, use the FT elements in fasthtml.svg,
discussed below.
Note: fasthtml.common does NOT automatically export SVG elements. To get access to
them, you need to import fasthtml.svg like so
source
Svg
Svg (*args, viewBox=None, h=None, w=None, height=None, width=None,
xmlns='http://www.w3.org/2000/svg', **kwargs)
An SVG tag; xmlns is added automatically, and viewBox defaults to height and width
if not provided
To create your own SVGs, use SVG. It will automatically set the viewBox from height
and width if not provided.
All of our shapes will have some convenient kwargs added by using ft_svg:
source
ft_svg
ft_svg (tag:str, *c, transform=None, opacity=None, clip=None, mask=None,
filter=None, vector_effect=None, pointer_events=None,
target_id=None, hx_vals=None, hx_target=None, id=None, cls=None,
title=None, style=None, accesskey=None, contenteditable=None,
dir=None, draggable=None, enterkeyhint=None, hidden=None,
inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
hx_select=None, hx_select_oob=None, hx_indicator=None,
hx_push_url=None, hx_confirm=None, hx_disable=None,
hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
hx_headers=None, hx_history=None, hx_history_elt=None,
hx_inherit=None, hx_params=None, hx_preserve=None,
hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None)
Create a standard FT element with some SVG-specific attrs
Basic shapes
We’ll define a simple function to display SVG shapes in this notebook:
source
Rect
Rect (width, height, x=0, y=0, fill=None, stroke=None, stroke_width=None,
rx=None, ry=None, transform=None, opacity=None, clip=None,
mask=None, filter=None, vector_effect=None, pointer_events=None,
target_id=None, hx_vals=None, hx_target=None, id=None, cls=None,
title=None, style=None, accesskey=None, contenteditable=None,
dir=None, draggable=None, enterkeyhint=None, hidden=None,
inert=None, inputmode=None, lang=None, popover=None,
spellcheck=None, tabindex=None, translate=None, hx_get=None,
hx_post=None, hx_put=None, hx_delete=None, hx_patch=None,
hx_trigger=None, hx_swap=None, hx_swap_oob=None, hx_include=None,
hx_select=None, hx_select_oob=None, hx_indicator=None,
hx_push_url=None, hx_confirm=None, hx_disable=None,
hx_replace_url=None, hx_disabled_elt=None, hx_ext=None,
hx_headers=None, hx_history=None, hx_history_elt=None,
hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None)
A standard SVG rect element
All our shapes just create regular FT elements. The only extra functionality
provided by most of them is to add additional defined kwargs to improve auto-
complete in IDEs and notebooks, and re-order parameters so that positional args can
also be used to save a bit of typing, e.g:
source
Circle
Circle (r, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,
transform=None, opacity=None, clip=None, mask=None, filter=None,
vector_effect=None, pointer_events=None, target_id=None,
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_get=None, hx_post=None,
hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None)
A standard SVG circle element
source
Ellipse
Ellipse (rx, ry, cx=0, cy=0, fill=None, stroke=None, stroke_width=None,
transform=None, opacity=None, clip=None, mask=None, filter=None,
vector_effect=None, pointer_events=None, target_id=None,
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_get=None, hx_post=None,
hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None)
A standard SVG ellipse element
source
transformd
transformd (translate=None, scale=None, rotate=None, skewX=None,
skewY=None, matrix=None)
Create an SVG transform kwarg dict
{'transform': 'rotate(45,25,25)'}
demo(Ellipse(20, 10, 25, 25, **rot))
source
Line
Line (x1, y1, x2=0, y2=0, stroke='black', w=None, stroke_width=1,
transform=None, opacity=None, clip=None, mask=None, filter=None,
vector_effect=None, pointer_events=None, target_id=None,
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_get=None, hx_post=None,
hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
hx_sync=None, hx_validate=None)
A standard SVG line element
source
Polyline
Polyline (*args, points=None, fill=None, stroke=None, stroke_width=None,
transform=None, opacity=None, clip=None, mask=None,
filter=None, vector_effect=None, pointer_events=None,
target_id=None, hx_vals=None, hx_target=None, id=None,
cls=None, title=None, style=None, accesskey=None,
contenteditable=None, dir=None, draggable=None,
enterkeyhint=None, hidden=None, inert=None, inputmode=None,
lang=None, popover=None, spellcheck=None, tabindex=None,
translate=None, hx_get=None, hx_post=None, hx_put=None,
hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap=None,
hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None)
A standard SVG polyline element
source
Polygon
Polygon (*args, points=None, fill=None, stroke=None, stroke_width=None,
transform=None, opacity=None, clip=None, mask=None, filter=None,
vector_effect=None, pointer_events=None, target_id=None,
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_get=None, hx_post=None,
hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None,
hx_request=None, hx_sync=None, hx_validate=None)
A standard SVG polygon element
source
Text
Text (*args, x=0, y=0, font_family=None, font_size=None, fill=None,
text_anchor=None, dominant_baseline=None, font_weight=None,
font_style=None, text_decoration=None, transform=None,
opacity=None, clip=None, mask=None, filter=None,
vector_effect=None, pointer_events=None, target_id=None,
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_get=None, hx_post=None,
hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
hx_sync=None, hx_validate=None)
A standard SVG text element
Hello!
Paths
Paths in SVGs are more complex, so we add a small (optional) fluent interface for
constructing them:
source
PathFT
PathFT (tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs)
A ‘Fast Tag’ structure, containing tag,children,and attrs
source
Path
Path (d='', fill=None, stroke=None, stroke_width=None, transform=None,
opacity=None, clip=None, mask=None, filter=None,
vector_effect=None, pointer_events=None, target_id=None,
hx_vals=None, hx_target=None, id=None, cls=None, title=None,
style=None, accesskey=None, contenteditable=None, dir=None,
draggable=None, enterkeyhint=None, hidden=None, inert=None,
inputmode=None, lang=None, popover=None, spellcheck=None,
tabindex=None, translate=None, hx_get=None, hx_post=None,
hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None,
hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None,
hx_select_oob=None, hx_indicator=None, hx_push_url=None,
hx_confirm=None, hx_disable=None, hx_replace_url=None,
hx_disabled_elt=None, hx_ext=None, hx_headers=None,
hx_history=None, hx_history_elt=None, hx_inherit=None,
hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None,
hx_sync=None, hx_validate=None)
Create a standard path SVG element. This is a special object
p = (Path(fill='red')
.M(25,45)
.C(25,45,10,35,10,25)
.A(15,15,0,1,1,40,25)
.C(40,35,25,45,25,45)
.Z())
demo(p)
Behind the scenes it’s just creating regular SVG path d attr – you can pass d in
directly if you prefer.
print(p.d)
source
PathFT.M
PathFT.M (x, y)
Move to.
source
PathFT.L
PathFT.L (x, y)
Line to.
source
PathFT.H
PathFT.H (x)
Horizontal line to.
source
PathFT.V
PathFT.V (y)
Vertical line to.
source
PathFT.Z
PathFT.Z ()
Close path.
source
PathFT.C
PathFT.C (x1, y1, x2, y2, x, y)
Cubic Bézier curve.
source
PathFT.S
PathFT.S (x2, y2, x, y)
Smooth cubic Bézier curve.
source
PathFT.Q
PathFT.Q (x1, y1, x, y)
Quadratic Bézier curve.
source
PathFT.T
PathFT.T (x, y)
Smooth quadratic Bézier curve.
source
PathFT.A
PathFT.A (rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, x, y)
Elliptical Arc.
HTMX helpers
source
SvgOob
SvgOob (*args, **kwargs)
Wraps an SVG shape as required for an HTMX OOB swap
When returning an SVG shape out-of-band (OOB) in HTMX, you need to wrap it with
SvgOob to have it appear correctly. (SvgOob is just a shortcut for
Template(Svg(...)), which is the trick that makes SVG OOB swaps work.)
source
SvgInb
SvgInb (*args, **kwargs)
Wraps an SVG shape as required for an HTMX inband swap
When returning an SVG shape in-band in HTMX, either have the calling element
include hx_select='svg>*', or **svg_inb (which are two ways of saying the same
thing), or wrap the response with SvgInb to have it appear correctly. (SvgInb is
just a shortcut for the tuple (Svg(...), HtmxResponseHeaders(hx_reselect='svg>*')),
which is the trick that makes SVG in-band swaps work.)
---
https://docs.fastht.ml/api/jupyter.html
Jupyter compatibility
Use FastHTML in Jupyter notebooks
from httpx import get, AsyncClient
Helper functions
source
nb_serve
nb_serve (app, log_level='error', port=8000, host='0.0.0.0', **kwargs)
Start a Jupyter compatible uvicorn server with ASGI app on port with log_level
source
nb_serve_async
nb_serve_async (app, log_level='error', port=8000, host='0.0.0.0',
**kwargs)
Async version of nb_serve
source
is_port_free
is_port_free (port, host='localhost')
Check if port is free on host
source
wait_port_free
wait_port_free (port, host='localhost', max_wait=3)
Wait for port to be free on host
show
show (*s)
Same as fasthtml.components.show, but also adds htmx.process()
source
render_ft
render_ft ()
source
htmx_config_port
htmx_config_port (port=8000)
source
JupyUvi
JupyUvi (app, log_level='error', host='0.0.0.0', port=8000, start=True,
**kwargs)
Start and stop a Jupyter compatible uvicorn server with ASGI app on port with
log_level
Creating an object of this class also starts the Uvicorn server. It runs in a
separate thread, so you can use normal HTTP client functions in a notebook.
app = FastHTML()
rt = app.route
@app.route
def index(): return 'hi'
port = 8000
server = JupyUvi(app, port=port)
get(f'http://localhost:{port}').text
'hi'
You can stop the server, modify routes, and start the server again without
restarting the notebook or recreating the server or application.
server.stop()
source
JupyUviAsync
JupyUviAsync (app, log_level='error', host='0.0.0.0', port=8000,
**kwargs)
Start and stop an async Jupyter compatible uvicorn server with ASGI app on port
with log_level
hi
server.stop()
show(*def_hdrs())
render_ft()
@rt
def hoho(): return P('loaded!'), Div('hee hee', id=c, hx_swap_oob='true')
not loaded
(c := Div(''))
@rt
def foo(): return Div('foo bar')
P('hi', hx_get=foo, hx_trigger='load', hx_target=c)
hi
server.stop()
source
HTMX
HTMX (path='', app=None, host='localhost', port=8000, height='auto',
link=False, iframe=True)
An iframe which displays the HTMX application in a notebook.
@rt
def index():
return Div(
P(A('Click me', hx_get=update, hx_target='#result')),
P(A('No me!', hx_get=update, hx_target='#result')),
Div(id='result'))
@rt
def update(): return Div(P('Hi!'),P('There!'))
server.start()
# Run the notebook locally to see the HTMX iframe in action
HTMX()
server.stop()
source
ws_client
ws_client (app, nm='', host='localhost', port=8000, ws_connect='/ws',
frame=True, link=True, **kwargs)
---
https://docs.fastht.ml/api/oauth.html
OAuth
Basic scaffolding for handling OAuth
See the docs page for an explanation of how to use this.
source
GoogleAppClient
GoogleAppClient (client_id, client_secret, code=None, scope=None,
**kwargs)
A WebApplicationClient for Google oauth2
source
GitHubAppClient
GitHubAppClient (client_id, client_secret, code=None, scope=None,
**kwargs)
A WebApplicationClient for GitHub oauth2
source
HuggingFaceClient
HuggingFaceClient (client_id, client_secret, code=None, scope=None,
state=None, **kwargs)
A WebApplicationClient for HuggingFace oauth2
source
DiscordAppClient
DiscordAppClient (client_id, client_secret, is_user=False, perms=0,
scope=None, **kwargs)
A WebApplicationClient for Discord oauth2
source
Auth0AppClient
Auth0AppClient (domain, client_id, client_secret, code=None, scope=None,
redirect_uri='', **kwargs)
A WebApplicationClient for Auth0 OAuth2
# cli = GoogleAppClient.from_file('/Users/jhoward/subs_aai/_nbs/oauth-test/
client_secret.json')
source
WebApplicationClient.login_link
WebApplicationClient.login_link (redirect_uri, scope=None, state=None)
Get a login link for this client
Generating a login link that sends the user to the OAuth provider is done with
client.login_link().
It can sometimes be useful to pass state to the OAuth provider, so that when the
user returns you can pick up where they left off. This can be done by passing the
state parameter.
redir_path = '/redirect'
port = 8000
code_stor = None
app,rt = fast_app()
server = JupyUvi(app, port=port)
source
redir_url
redir_url (request, redir_path, scheme=None)
Get the redir url for the host in request
@rt
def index(request):
redir = redir_url(request, redir_path)
return A('login', href=cli.login_link(redir), target='_blank')
source
_AppClient.parse_response
_AppClient.parse_response (code, redirect_uri)
Get the token from the oauth2 server response
source
_AppClient.get_info
_AppClient.get_info (token=None)
Get the info for authenticated user
source
_AppClient.retr_info
_AppClient.retr_info (code, redirect_uri)
Combines parse_response and get_info
@rt(redir_path)
def get(request, code:str):
redir = redir_url(request, redir_path)
info = cli.retr_info(code, redir)
return P(f'Login successful for {info["name"]}!')
# HTMX()
server.stop()
source
_AppClient.retr_id
_AppClient.retr_id (code, redirect_uri)
Call retr_info and then return id/subscriber value
After logging in via the provider, the user will be redirected back to the supplied
redirect URL. The request to this URL will contain a code parameter, which is used
to get an access token and fetch the user’s profile information. See the
explanation here for a worked example. You can either:
source
url_match
url_match (url, patterns=('^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',))
source
OAuth
OAuth (app, cli, skip=None, redir_path='/redirect', error_path='/error',
logout_path='/logout', login_path='/login', https=True,
http_patterns=('^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',))
Initialize self. See help(type(self)) for accurate signature.
---
https://docs.fastht.ml/api/cli.html
railway_link
railway_link ()
Link the current directory to the current project’s Railway service
source
railway_deploy
railway_deploy (name:str, mount:<function bool_arg>=True)
Deploy a FastHTML app to Railway