import time
from IPython import display
from enum import Enum
from pprint import pprint
from fastcore.test import *
from starlette.testclient import TestClient
from starlette.requests import Headers
from starlette.datastructures import UploadFile
Core
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.
Imports and utils
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.
parsed_date
parsed_date (s:str)
Convert s
to a datetime
'2pm') parsed_date(
datetime.datetime(2025, 1, 12, 14, 0)
isinstance(date.fromtimestamp(0), date)
True
snake2hyphens
snake2hyphens (s:str)
Convert s
from snake case to hyphenated and capitalised
"snake_case") snake2hyphens(
'Snake-Case'
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),
}= lambda: {"body": b"", "more_body": False}
receive return Request(scope, receive)
= test_request(headers=Headers({'HX-Request':'1'}))
h _get_htmx(h.headers)
HtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None)
Request and response
str,None], 'a'), 'a')
test_eq(_fix_anno(Union[float, 0.9), 0.9)
test_eq(_fix_anno(int, '1'), 1)
test_eq(_fix_anno(int, ['1','2']), 2)
test_eq(_fix_anno(list[int], ['1','2']), [1,2])
test_eq(_fix_anno(list[int], '1'), [1]) test_eq(_fix_anno(
= dict(k=int, l=List[int])
d 'k', "1", d), 1)
test_eq(_form_arg('l', "1", d), [1])
test_eq(_form_arg('l', ["1","2"], d), [1,2]) test_eq(_form_arg(
HttpHeader
HttpHeader (k:str, v:str)
'trigger_after_settle') _to_htmx_header(
'HX-Trigger-After-Settle'
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
='hi') HtmxResponseHeaders(trigger_after_settle
HttpHeader(k='HX-Trigger-After-Settle', v='hi')
form2dict
form2dict (form:starlette.datastructures.FormData)
Convert starlette form data to a dict
= [('a',1),('a',2),('b',0)]
d = FormData(d)
fd = form2dict(fd)
res 'a'], [1,2])
test_eq(res['b'], 0) test_eq(res[
parse_form
parse_form (req:starlette.requests.Request)
Starlette errors on empty multipart forms, so this checks for that situation
async def f(req):
def _f(p:HttpHeader): ...
= first(_params(_f).values())
p = await _from_body(req, p)
result return JSONResponse(result.__dict__)
= TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
client
= dict(k='value1',v=['value2','value3'])
d = client.post('/', data=d)
response print(response.json())
{'k': 'value1', 'v': 'value3'}
async def f(req): return Response(str(req.query_params.getlist('x')))
= TestClient(Starlette(routes=[Route('/', f, methods=['GET'])]))
client '/?x=1&x=2').text client.get(
"['1', '2']"
def g(req, this:Starlette, a:str, b:HttpHeader): ...
async def f(req):
= await _wrap_req(req, _params(g))
a return Response(str(a))
= TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
client = client.post('/?a=1', data=d)
response print(response.text)
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]
def g(req, this:Starlette, a:str, b:HttpHeader): ...
async def f(req):
= await _wrap_req(req, _params(g))
a return Response(str(a))
= TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
client = client.post('/?a=1', data=d)
response print(response.text)
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]
flat_xt
flat_xt (lst)
Flatten lists
= ft('a',1)
x *4)
test_eq(flat_xt([x, x, [x,x]]), (x,) test_eq(flat_xt(x), (x,))
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}"
= _ws_endp(on_receive)
c = TestClient(Starlette(routes=[WebSocketRoute('/', _ws_endp(on_receive))]))
cli with cli.websocket_connect('/') as ws:
'{"msg":"Hi!"}')
ws.send_text(= ws.receive_text()
data assert data == 'Message text was: Hi!'
EventStream
EventStream (s)
Create a text/event-stream response from s
signal_shutdown
signal_shutdown ()
Routing and application
uri
uri (_arg, **kwargs)
decode_uri
decode_uri (s)
StringConvertor.to_string
StringConvertor.to_string (value:str)
HTTPConnection.url_path_for
HTTPConnection.url_path_for (name:str, **path_params)
flat_tuple
flat_tuple (o)
Flatten lists
noop_body
noop_body (c, req)
Default Body wrap function which just returns the content
respond
respond (req, heads, bdy)
Default FT response creation function
Redirect
Redirect (loc)
Use HTMX or Starlette RedirectResponse as required to redirect to loc
get_key
get_key (key=None, fname='.sesskey')
get_key()
'5a5e5544-5ee8-46f2-836e-924976ce8b58'
qp
qp (p:str, **kw)
Add query parameters to path p
'/foo', a=None, b=False, c=[1,2], d='bar') qp(
'/foo?a=&b=&c=1&c=2&d=bar'
def_hdrs
def_hdrs (htmx=True, surreal=True)
Default headers for a FastHTML app
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.
FastHTML.ws
FastHTML.ws (path:str, conn=None, disconn=None, name=None, middleware=None)
Add a websocket route at path
nested_name
nested_name (f)
*Get name of function f
using ’_’ to join nested function names*
def f():
def g(): ...
return g
= f()
func nested_name(func)
'f_g'
FastHTML.route
FastHTML.route (path:str=None, methods=None, name=None, include_in_schema=True, body_wrap=None)
Add a route at path
= FastHTML()
app @app.get
def foo(a:str, b:list[int]): ...
print(app.routes)
='bar', b=[1,2]) foo.to(a
[Route(path='/foo', name='foo', methods=['GET', 'HEAD'])]
'/foo?a=bar&b=1&b=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.
Type | Default | Details | |
---|---|---|---|
appname | NoneType | None | Name of the module |
app | str | app | App instance to be served |
host | str | 0.0.0.0 | If host is 0.0.0.0 will convert to localhost |
port | NoneType | None | If port is None it will default to 5001 or the PORT environment variable |
reload | bool | True | Default is to reload the app upon code changes |
reload_includes | list[str] | str | None | None | Additional files to watch for changes |
reload_excludes | list[str] | str | None | None | Files to ignore for changes |
Client
Client (app, url='http://testserver')
A simple httpx ASGI client that doesn’t require async
= FastHTML(routes=[Route('/', lambda _: Response('test'))])
app = Client(app)
cli
'/').text cli.get(
'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
= get_cli(FastHTML(secret_key='soopersecret')) app,cli,rt
= get_cli(FastHTML(title="My Custom Title"))
app,cli,rt @app.get
def foo(): return Div("Hello World")
print(app.routes)
= cli.get('/foo')
response assert '<title>My Custom Title</title>' in response.text
='value') foo.to(param
[Route(path='/foo', name='foo', methods=['GET', 'HEAD'])]
'/foo?param=value'
= get_cli(FastHTML())
app,cli,rt
@rt('/xt2')
def get(): return H1('bar')
= cli.get('/xt2').text
txt assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
@rt("/hi")
def get(): return 'Hi there'
= cli.get('/hi')
r r.text
'Hi there'
@rt("/hi")
def post(): return 'Postal'
'/hi').text cli.post(
'Postal'
@app.get("/hostie")
def show_host(req): return req.headers['host']
'/hostie').text cli.get(
'testserver'
@app.get("/setsess")
def set_sess(session):
'foo'] = 'bar'
session[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}"
'/setsess')
cli.get(with cli.websocket_connect('/ws') as ws:
'{"msg":"Hi!"}')
ws.send_text(= ws.receive_text()
data 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'
'/yoyo').text cli.post(
'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}!"
'/user/Alexis').text cli.get(
'Good day to you, Alexis!'
@app.get
def autolink(): return Html(Div('Text.', link=uri('gday', nm='Alexis')))
print(cli.get('/autolink').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')}"
'/link').text cli.get(
'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)
= cli.get("/background") response
Background task completed!
'gday', nm='Jeremy'), '/user/Jeremy') test_eq(app.router.url_path_for(
= {'headers':{'hx-request':"1"}}
hxhdr
@rt('/ft')
def get(): return Title('Foo'),H1('bar')
= cli.get('/ft').text
txt assert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
@rt('/xt2')
def get(): return H1('bar')
= cli.get('/xt2').text
txt assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
assert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'
@rt('/xt3')
def get(): return Html(Head(Title('hi')), Body(P('there')))
= cli.get('/xt3').text
txt 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
lambda: cli.get('/oops?nope=1')) test_warns(
def test_r(cli, path, exp, meth='get', hx=False, **kwargs):
if hx: kwargs['headers'] = {'hx-request':"1"}
getattr(cli, meth)(path, **kwargs).text, exp)
test_eq(
= str_enum('ModelName', "alexnet", "resnet", "lenet")
ModelName = [{"name": "Foo"}, {"name": "Bar"}] fake_db
@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)
= cli.get('/html/1', headers={'hx-request':"1"})
r assert '<h4>Next is 2.</h4>' in r.text
'/models/alexnet', 'alexnet')
test_r(cli, '/files/foo', 'foo.txt')
test_r(cli, '/items/?idx=1', '{"name":"Bar"}')
test_r(cli, '/items/', '{"name":"Foo"}')
test_r(cli, assert cli.get('/items/?idx=g').text=='404 Not Found'
assert cli.get('/items/?idx=g').status_code == 404
'/idxl/?idx=1&idx=2', '[1, 2]')
test_r(cli, assert cli.get('/idxl/?idx=1&idx=g').status_code == 404
= FastHTML()
app = app.route
rt = TestClient(app)
cli @app.route(r'/static/{path:path}.jpg')
def index(path:str): return f'got {path}'
'/static/sub/a.b.jpg').text cli.get(
'got sub/a.b'
= 'foo' app.chk
@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")
'/booly/?coming=true', 'Coming')
test_r(cli, '/booly/?coming=no', 'Not coming')
test_r(cli, = "17th of May, 2024, 2p"
date_str f'/datie/?d={date_str}', '2024-05-17 14:00:00')
test_r(cli, '/ua', 'FastHTML', headers={'User-Agent':'FastHTML'})
test_r(cli, '/hxtest' , '1', headers={'HX-Request':'1'})
test_r(cli, '/hxtest2', '1', headers={'HX-Request':'1'})
test_r(cli, '/app' , 'foo') test_r(cli,
= cli.get('/app2', **hxhdr)
r 'foo')
test_eq(r.text, 'mykey'], 'myval') test_eq(r.headers[
= cli.get('/app3')
r 'HX-Location'], 'http://example.org') test_eq(r.headers[
= cli.get('/app4', follow_redirects=False)
r 303) test_eq(r.status_code,
= cli.get('/app4', headers={'HX-Request':'1'})
r 'HX-Redirect'], 'http://example.org') test_eq(r.headers[
@rt
def meta():
return ((Title('hi'),H1('hi')),
property='image'), Meta(property='site_name'))
(Meta(
)
= cli.post('/meta').text
t 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
'/profile/me', 'Alexis', 'post', data={'username' : 'Alexis'})
test_r(cli, '/profile/me', 'Missing required field: username', 'post', data={}) test_r(cli,
# Example post request with parameter that has a default value
@app.post('/pet/dog')
def pet_dog(dogname: str = None): return dogname
# Working post request with optional parameter
'/pet/dog', '', 'post', data={}) test_r(cli,
@dataclass
class Bodie: a:int;b:str
@rt("/bodie/{nm}")
def post(nm:str, data:Bodie):
= asdict(data)
res 'nm'] = nm
res[return res
@app.post("/bodied/")
def bodied(data:dict): return data
= namedtuple('Bodient', ['a','b'])
nt
@app.post("/bodient/")
def bodient(data:nt): return asdict(data)
class BodieTD(TypedDict): a:int;b:str='foo'
@app.post("/bodietd/")
def bodient(data:BodieTD): return data
class Bodie2:
int|None; b:str
a: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}"
from fasthtml.xtend import Titled
= dict(a=1, b='foo')
d
'/bodie/me', '{"a":1,"b":"foo","nm":"me"}', 'post', data=dict(a=1, b='foo', nm='me'))
test_r(cli, '/bodied/', '{"a":"1","b":"foo"}', 'post', data=d)
test_r(cli, '/bodie2/', 'a: 1; b: foo', 'post', data={'a':1})
test_r(cli, '/bodie2/?a=1&b=foo&nm=me', 'a: 1; b: foo')
test_r(cli, '/bodient/', '{"a":"1","b":"foo"}', 'post', data=d)
test_r(cli, '/bodietd/', '{"a":1,"b":"foo"}', 'post', data=d) test_r(cli,
# Testing POST with Content-Type: application/json
@app.post("/")
def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))
= json.dumps({"b": "Lorem", "a": 15})
s = cli.post('/', headers={"Content-Type": "application/json"}, data=s).text
response assert "<title>It worked!</title>" in response and "<p>15, Lorem</p>" in response
# Testing POST with Content-Type: application/json
@app.post("/bodytext")
def index(body): return body
= cli.post('/bodytext', headers={"Content-Type": "application/json"}, data=s).text
response '{"b": "Lorem", "a": 15}') test_eq(response,
= [ ('files', ('file1.txt', b'content1')),
files 'files', ('file2.txt', b'content2')) ] (
@rt("/uploads")
async def post(files:list[UploadFile]):
return ','.join([(await file.read()).decode() for file in files])
= cli.post('/uploads', files=files)
res print(res.status_code)
print(res.text)
200
content1,content2
= cli.post('/uploads', files=[files[0]])
res print(res.status_code)
print(res.text)
200
content1
@rt("/setsess")
def get(sess, foo:str=''):
= datetime.now()
now 'auth'] = str(now)
sess[return f'Set to {now}'
@rt("/getsess")
def get(sess): return f'Session time: {sess["auth"]}'
print(cli.get('/setsess').text)
0.01)
time.sleep(
'/getsess').text cli.get(
Set to 2025-01-12 14:12:46.576323
'Session time: 2025-01-12 14:12:46.576323'
@rt("/sess-first")
def post(sess, name: str):
"name"] = name
sess[return str(sess)
'/sess-first', data={'name': 2})
cli.post(
@rt("/getsess-all")
def get(sess): return sess['name']
'/getsess-all').text, '2') test_eq(cli.get(
@rt("/upload")
async def post(uf:UploadFile): return (await uf.read()).decode()
with open('../../CHANGELOG.md', 'rb') as f:
print(cli.post('/upload', files={'uf':f}, data={'msg':'Hello'}).text[:15])
# 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)
= cli.options('/form-submit/2').headers
h 'Access-Control-Allow-Methods'], 'POST') test_eq(h[
from fasthtml.authmw import user_pwd_auth
def _not_found(req, exc): return Div('nope')
= get_cli(FastHTML(exception_handlers={404:_not_found}))
app,cli,rt
= cli.get('/').text
txt assert '<div>nope</div>' in txt
assert '<!doctype html>' in txt
= get_cli(FastHTML())
app,cli,rt
@rt("/{name}/{age}")
def get(name: str, age: int):
return Titled(f"Hello {name.title()}, age {age}")
assert '<title>Hello Uma, age 5</title>' in cli.get('/uma/5').text
assert '404 Not Found' in cli.get('/uma/five').text
= user_pwd_auth(testuser='spycraft')
auth = get_cli(FastHTML(middleware=[auth]))
app,cli,rt
@rt("/locked")
def get(auth): return 'Hello, ' + auth
'/locked').text, 'not authenticated')
test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser') test_eq(cli.get(
= user_pwd_auth(testuser='spycraft')
auth = get_cli(FastHTML(middleware=[auth]))
app,cli,rt
@rt("/locked")
def get(auth): return 'Hello, ' + auth
'/locked').text, 'not authenticated')
test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser') test_eq(cli.get(
APIRouter
RouteFuncs
RouteFuncs ()
Initialize self. See help(type(self)) for accurate signature.
APIRouter
APIRouter (prefix:str|None=None, body_wrap=<function noop_body>)
Add routes to an app
= APIRouter() ar
@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}"
= get_cli(FastHTML())
app,cli,_ ar.to_app(app)
assert str(yoyo) == '/yoyo'
# ensure route functions are properly discoverable on `APIRouter` and `APIRouter.rt_funcs`
assert ar.prefix == ''
assert str(ar.rt_funcs.index) == '/'
assert str(ar.index) == '/'
with ExceptionExpected(): ar.blah()
with ExceptionExpected(): ar.rt_funcs.blah()
# ensure any route functions named using an HTTPMethod are not discoverable via `rt_funcs`
assert "get" not in ar.rt_funcs._funcs.keys()
'/hi').text, 'Hi there')
test_eq(cli.get('/hi').text, 'Postal')
test_eq(cli.post('/hostie').text, 'testserver')
test_eq(cli.get('/yoyo').text, 'a yoyo')
test_eq(cli.post(
'/ho').text, 'Ho ho')
test_eq(cli.get('/ho').text, 'Ho ho') test_eq(cli.post(
with cli.websocket_connect('/ws') as ws:
'{"msg":"Hi!"}')
ws.send_text(= ws.receive_text()
data assert data == 'Message text was: Hi!'
= APIRouter("/products") ar2
@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}"
= get_cli(FastHTML())
app,cli,_ ar2.to_app(app)
assert str(yoyo) == '/products/yoyo'
assert ar2.prefix == '/products'
assert str(ar2.rt_funcs.index) == '/products/'
assert str(ar2.index) == '/products/'
assert str(ar.index) == '/'
with ExceptionExpected(): ar2.blah()
with ExceptionExpected(): ar2.rt_funcs.blah()
assert "get" not in ar2.rt_funcs._funcs.keys()
'/products/hi').text, 'Hi there')
test_eq(cli.get('/products/hi').text, 'Postal')
test_eq(cli.post('/products/hostie').text, 'testserver')
test_eq(cli.get('/products/yoyo').text, 'a yoyo')
test_eq(cli.post(
'/products/ho').text, 'Ho ho')
test_eq(cli.get('/products/ho').text, 'Ho ho') test_eq(cli.post(
with cli.websocket_connect('/products/ws') as ws:
'{"msg":"Hi!"}')
ws.send_text(= ws.receive_text()
data assert data == 'Message text was: Hi!'
@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
= get_cli(FastHTML(secret_key='soopersecret')) app,cli,rt
reg_re_param
reg_re_param (m, s)
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()
"imgext", "ico|gif|jpg|jpeg|webm|pdf")
reg_re_param(
@rt(r'/static/{path:path}{fn}.{ext:imgext}')
def get(fn:str, path:str, ext:str): return f"Getting {fn}.{ext} from /{path}"
'/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/') test_r(cli,
app.static_route_exts()assert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text
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 ‘.’)
'.md', static_path='../..')
app.static_route(assert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text
MiddlewareBase
MiddlewareBase ()
Initialize self. See help(type(self)) for accurate signature.
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():
= Title('Foo'),H1('bar')
cts return FtResponse(cts, status_code=201, headers={'Location':'/foo/1'})
= cli.get('/ftr')
r
201)
test_eq(r.status_code, 'location'], '/foo/1')
test_eq(r.headers[= r.text
txt assert '<title>Foo</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt
unqid
unqid ()
setup_ws
setup_ws (app, f=<function noop>)