I’ve been experimenting with FastHTML for making quick demo apps, often involving language models.
It’s a pretty simple but powerful framework, which allows me to deploy a client and server in a single main.py
– something I appreciate a lot for little projects I want to ship quickly.
I currently use it how you might use streamlit
.
I ran into an issue where I was struggling to submit a form with multiple images.
I started with an app that could receive a single image upload from this example.
These examples assume the code is in a main.py
file and run with
uvicorn main:app --reload
from fasthtml.common import *
app, rt = fast_app()
@rt("/")
def get():
inp = Input(type="file", name="image", multiple=False, required=True)
add = Form(
Group(inp, Button("Upload")),
hx_post="/upload",
hx_target="#image-list",
hx_swap="afterbegin",
enctype="multipart/form-data",
)
image_list = Div(id="image-list")
return Title("Image Upload Demo"), Main(
H1("Image Upload"), add, image_list, cls="container"
)
@rt("/upload")
async def upload_image(image: UploadFile):
contents = await image.read()
print(contents)
filename = image.filename
return filename
The contents of the images prints in the console and the filename shows up in the browser.
To support multiple files/images, I tried the following:
from fasthtml.common import *
import uvicorn
import os
app, rt = fast_app()
@rt("/")
def get():
inp = Input(type="file", name="images", multiple=True, required=True)
add = Form(
Group(inp, Button("Upload")),
hx_post="/upload",
hx_target="#image-list",
hx_swap="afterbegin",
enctype="multipart/form-data",
)
image_list = Div(id="image-list")
return Title("Image Upload Demo"), Main(
H1("Image Upload"), add, image_list, cls="container"
)
@rt("/upload")
async def upload_image(images: List[UploadFile]):
print(images)
filenames = []
for image in images:
contents = await image.read()
filename = image.filename
filenames.append(filename)
return filenames
When we pick and upload multiple files, this code breaks, but with the print statement we can see the data we uploaded.
[UploadFile(filename=None, size=None, headers=Headers({})), UploadFile(filename=None, size=None, headers=Headers({}))]
Not quite what I expected.
After a bit of searching, I learned that a fasthtml
function signature can be any compatible starlette
function signature (source).
With this knowledge, I tried the following approach:
from fasthtml.common import *
app, rt = fast_app()
@rt("/")
def get():
inp = Input(
type="file", name="images", multiple=True, required=True, accept="image/*"
)
add = Form(
Group(inp, Button("Upload")),
hx_post="/upload",
hx_target="#image-list",
hx_swap="afterbegin",
enctype="multipart/form-data",
)
image_list = Div(id="image-list")
return Title("Image Upload Demo"), Main(
H1("Image Upload"), add, image_list, cls="container"
)
@rt("/upload")
async def upload_image(request: Request):
form = await request.form()
images = form.getlist("images")
print(images)
filenames = []
for image in images:
contents = await image.read()
filenames.append(image.filename)
return filenames
This approach successfully rendered the titles of two images when I uploaded them in a single request as expected.