home-fasthtml / docs /api /core.html.md
AItool's picture
Upload 210 files
036b3a6 verified
# Core
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
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](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py)
using [nbdev](https://nbdev.fast.ai/).
## Imports and utils
``` python
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
```
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`](https://www.fastht.ml/docs/api/core.html#parsed_date),
is an example of this pattern.
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L45"
target="_blank" style="float:right; font-size:smaller">source</a>
### parsed_date
> parsed_date (s:str)
*Convert `s` to a datetime*
``` python
parsed_date('2pm')
```
datetime.datetime(2025, 5, 29, 14, 0)
``` python
isinstance(date.fromtimestamp(0), date)
```
True
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L50"
target="_blank" style="float:right; font-size:smaller">source</a>
### snake2hyphens
> snake2hyphens (s:str)
*Convert `s` from snake case to hyphenated and capitalised*
``` python
snake2hyphens("snake_case")
```
'Snake-Case'
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L67"
target="_blank" style="float:right; font-size:smaller">source</a>
### 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)
``` python
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)
```
``` python
h = test_request(headers=Headers({'HX-Request':'1'}))
_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
``` python
test_eq(_fix_anno(Union[str,None], 'a'), 'a')
test_eq(_fix_anno(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])
```
``` python
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])
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L106"
target="_blank" style="float:right; font-size:smaller">source</a>
### HttpHeader
> HttpHeader (k:str, v:str)
``` python
_to_htmx_header('trigger_after_settle')
```
'HX-Trigger-After-Settle'
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L117"
target="_blank" style="float:right; font-size:smaller">source</a>
### 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*
``` python
HtmxResponseHeaders(trigger_after_settle='hi')
```
HttpHeader(k='HX-Trigger-After-Settle', v='hi')
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L139"
target="_blank" style="float:right; font-size:smaller">source</a>
### form2dict
> form2dict (form:starlette.datastructures.FormData)
*Convert starlette form data to a dict*
``` python
d = [('a',1),('a',2),('b',0)]
fd = FormData(d)
res = form2dict(fd)
test_eq(res['a'], [1,2])
test_eq(res['b'], 0)
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L145"
target="_blank" style="float:right; font-size:smaller">source</a>
### parse_form
> parse_form (req:starlette.requests.Request)
*Starlette errors on empty multipart forms, so this checks for that
situation*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L168"
target="_blank" style="float:right; font-size:smaller">source</a>
### JSONResponse
> JSONResponse (content:Any, status_code:int=200,
> headers:Optional[Mapping[str,str]]=None,
> media_type:str|None=None,
> background:starlette.background.BackgroundTask|None=None)
*Same as starlette’s version, but auto-stringifies non serializable
types*
``` python
async def f(req):
def _f(p:HttpHeader): ...
p = first(_params(_f).values())
result = await _from_body(req, p)
return JSONResponse(result.__dict__)
client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
d = dict(k='value1',v=['value2','value3'])
response = client.post('/', data=d)
print(response.json())
```
{'k': 'value1', 'v': 'value3'}
``` python
async def f(req): return Response(str(req.query_params.getlist('x')))
client = TestClient(Starlette(routes=[Route('/', f, methods=['GET'])]))
client.get('/?x=1&x=2').text
```
"['1', '2']"
``` python
def g(req, this:Starlette, a:str, b:HttpHeader): ...
async def f(req):
a = await _wrap_req(req, _params(g))
return Response(str(a))
client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
response = client.post('/?a=1', data=d)
print(response.text)
```
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]
``` python
def g(req, this:Starlette, a:str, b:HttpHeader): ...
async def f(req):
a = await _wrap_req(req, _params(g))
return Response(str(a))
client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
response = client.post('/?a=1', data=d)
print(response.text)
```
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '1', HttpHeader(k='value1', v='value3')]
**Missing Request Params**
If a request param has a default value (e.g. `a:str=''`), the request is
valid even if the user doesn’t include the param in their request.
``` python
def g(req, this:Starlette, a:str=''): ...
async def f(req):
a = await _wrap_req(req, _params(g))
return Response(str(a))
client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
response = client.post('/', json={}) # no param in request
print(response.text)
```
[<starlette.requests.Request object>, <starlette.applications.Starlette object>, '']
If we remove the default value and re-run the request, we should get the
following error `Missing required field: a`.
``` python
def g(req, this:Starlette, a:str): ...
async def f(req):
a = await _wrap_req(req, _params(g))
return Response(str(a))
client = TestClient(Starlette(routes=[Route('/', f, methods=['POST'])]))
response = client.post('/', json={}) # no param in request
print(response.text)
```
Missing required field: a
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L218"
target="_blank" style="float:right; font-size:smaller">source</a>
### flat_xt
> flat_xt (lst)
*Flatten lists*
``` python
x = ft('a',1)
test_eq(flat_xt([x, x, [x,x]]), (x,)*4)
test_eq(flat_xt(x), (x,))
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L228"
target="_blank" style="float:right; font-size:smaller">source</a>
### Beforeware
> Beforeware (f, skip=None)
*Initialize self. See help(type(self)) for accurate signature.*
## Websockets / SSE
``` python
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!'
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L290"
target="_blank" style="float:right; font-size:smaller">source</a>
### EventStream
> EventStream (s)
*Create a text/event-stream response from `s`*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L295"
target="_blank" style="float:right; font-size:smaller">source</a>
### signal_shutdown
> signal_shutdown ()
## Routing and application
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L306"
target="_blank" style="float:right; font-size:smaller">source</a>
### uri
> uri (_arg, **kwargs)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L310"
target="_blank" style="float:right; font-size:smaller">source</a>
### decode_uri
> decode_uri (s)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L321"
target="_blank" style="float:right; font-size:smaller">source</a>
### StringConvertor.to_string
> StringConvertor.to_string (value:str)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L329"
target="_blank" style="float:right; font-size:smaller">source</a>
### HTTPConnection.url_path_for
> HTTPConnection.url_path_for (name:str, **path_params)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L367"
target="_blank" style="float:right; font-size:smaller">source</a>
### flat_tuple
> flat_tuple (o)
*Flatten lists*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L378"
target="_blank" style="float:right; font-size:smaller">source</a>
### noop_body
> noop_body (c, req)
*Default Body wrap function which just returns the content*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L383"
target="_blank" style="float:right; font-size:smaller">source</a>
### respond
> respond (req, heads, bdy)
*Default FT response creation function*
Render fragment if `HX-Request` header is *present* and
`HX-History-Restore-Request` header is *absent.*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L392"
target="_blank" style="float:right; font-size:smaller">source</a>
### is_full_page
> is_full_page (req, resp)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L446"
target="_blank" style="float:right; font-size:smaller">source</a>
### Redirect
> Redirect (loc)
*Use HTMX or Starlette RedirectResponse as required to redirect to
`loc`*
The FastHTML `exts` param supports the following:
``` python
print(' '.join(htmx_exts))
```
morph head-support preload class-tools loading-states multi-swap path-deps remove-me ws chunked-transfer
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L481"
target="_blank" style="float:right; font-size:smaller">source</a>
### get_key
> get_key (key=None, fname='.sesskey')
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L502"
target="_blank" style="float:right; font-size:smaller">source</a>
### qp
> qp (p:str, **kw)
*Add parameters kw to path p*
[`qp`](https://www.fastht.ml/docs/api/core.html#qp) adds query
parameters to route path strings
``` python
vals = {'a':5, 'b':False, 'c':[1,2], 'd':'bar', 'e':None, 'ab':42}
```
``` python
res = qp('/foo', **vals)
test_eq(res, '/foo?a=5&b=&c=1&c=2&d=bar&e=&ab=42')
```
[`qp`](https://www.fastht.ml/docs/api/core.html#qp) checks to see if
each param should be sent as a query parameter or as part of the route,
and encodes that properly.
``` python
path = '/foo/{a}/{d}/{ab:int}'
res = qp(path, **vals)
test_eq(res, '/foo/5/bar/42?b=&c=1&c=2&e=')
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L514"
target="_blank" style="float:right; font-size:smaller">source</a>
### def_hdrs
> def_hdrs (htmx=True, surreal=True)
*Default headers for a FastHTML app*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L536"
target="_blank" style="float:right; font-size:smaller">source</a>
### 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, canonical=True,
> **bodykw)
*Creates an Starlette application.*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L616"
target="_blank" style="float:right; font-size:smaller">source</a>
### FastHTML.ws
> FastHTML.ws (path:str, conn=None, disconn=None, name=None,
> middleware=None)
*Add a websocket route at `path`*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L631"
target="_blank" style="float:right; font-size:smaller">source</a>
### nested_name
> nested_name (f)
\*Get name of function `f` using ’\_’ to join nested function names\*
``` python
def f():
def g(): ...
return g
```
``` python
func = f()
nested_name(func)
```
'f_g'
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L652"
target="_blank" style="float:right; font-size:smaller">source</a>
### FastHTML.route
> FastHTML.route (path:str=None, methods=None, name=None,
> include_in_schema=True, body_wrap=None)
*Add a route at `path`*
``` python
app = FastHTML()
@app.get
def foo(a:str, b:list[int]): ...
foo.to(a='bar', b=[1,2])
```
'/foo?a=bar&b=1&b=2'
``` python
@app.get('/foo/{a}')
def foo(a:str, b:list[int]): ...
foo.to(a='bar', b=[1,2])
```
'/foo/bar?b=1&b=2'
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L660"
target="_blank" style="float:right; font-size:smaller">source</a>
### 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.*
<table>
<colgroup>
<col style="width: 6%" />
<col style="width: 25%" />
<col style="width: 34%" />
<col style="width: 34%" />
</colgroup>
<thead>
<tr>
<th></th>
<th><strong>Type</strong></th>
<th><strong>Default</strong></th>
<th><strong>Details</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>appname</td>
<td>NoneType</td>
<td>None</td>
<td>Name of the module</td>
</tr>
<tr>
<td>app</td>
<td>str</td>
<td>app</td>
<td>App instance to be served</td>
</tr>
<tr>
<td>host</td>
<td>str</td>
<td>0.0.0.0</td>
<td>If host is 0.0.0.0 will convert to localhost</td>
</tr>
<tr>
<td>port</td>
<td>NoneType</td>
<td>None</td>
<td>If port is None it will default to 5001 or the PORT environment
variable</td>
</tr>
<tr>
<td>reload</td>
<td>bool</td>
<td>True</td>
<td>Default is to reload the app upon code changes</td>
</tr>
<tr>
<td>reload_includes</td>
<td>list[str] | str | None</td>
<td>None</td>
<td>Additional files to watch for changes</td>
</tr>
<tr>
<td>reload_excludes</td>
<td>list[str] | str | None</td>
<td>None</td>
<td>Files to ignore for changes</td>
</tr>
</tbody>
</table>
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L683"
target="_blank" style="float:right; font-size:smaller">source</a>
### Client
> Client (app, url='http://testserver')
*A simple httpx ASGI client that doesn’t require `async`*
``` python
app = FastHTML(routes=[Route('/', lambda _: Response('test'))])
cli = Client(app)
cli.get('/').text
```
'test'
Note that you can also use Starlette’s `TestClient` instead of
FastHTML’s [`Client`](https://www.fastht.ml/docs/api/core.html#client).
They should be largely interchangable.
## FastHTML Tests
``` python
def get_cli(app): return app,TestClient(app),app.route
```
``` python
app,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))
```
``` python
app,cli,rt = get_cli(FastHTML(title="My Custom Title"))
@app.get
def foo(): return Div("Hello World")
print(app.routes)
response = cli.get('/foo')
assert '<title>My Custom Title</title>' in response.text
foo.to(param='value')
```
[Route(path='/foo', name='foo', methods=['GET', 'HEAD'])]
'/foo?param=value'
``` python
app,cli,rt = get_cli(FastHTML())
@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
```
``` python
@rt("/hi")
def get(): return 'Hi there'
r = cli.get('/hi')
r.text
```
'Hi there'
``` python
@rt("/hi")
def post(): return 'Postal'
cli.post('/hi').text
```
'Postal'
``` python
@app.get("/hostie")
def show_host(req): return req.headers['host']
cli.get('/hostie').text
```
'testserver'
``` python
@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)
``` python
@rt
def yoyo(): return 'a yoyo'
cli.post('/yoyo').text
```
'a yoyo'
``` python
@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>
``` python
@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>
``` python
@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>
``` python
@rt('/user/{nm}', name='gday')
def get(nm:str=''): return f"Good day to you, {nm}!"
cli.get('/user/Alexis').text
```
'Good day to you, Alexis!'
``` python
@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>
``` python
@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'
``` python
@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")
```
Background task completed!
``` python
test_eq(app.router.url_path_for('gday', nm='Jeremy'), '/user/Jeremy')
```
``` python
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
assert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'
@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
```
``` python
@rt('/oops')
def get(nope): return nope
test_warns(lambda: cli.get('/oops?nope=1'))
```
``` python
def test_r(cli, path, exp, meth='get', hx=False, **kwargs):
if hx: kwargs['headers'] = {'hx-request':"1"}
test_eq(getattr(cli, meth)(path, **kwargs).text, exp)
ModelName = str_enum('ModelName', "alexnet", "resnet", "lenet")
fake_db = [{"name": "Foo"}, {"name": "Bar"}]
```
``` python
@rt('/html/{idx}')
async def get(idx:int): return Body(H4(f'Next is {idx+1}.'))
```
``` python
@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)
```
``` python
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
```
``` python
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'
``` python
app.chk = 'foo'
```
``` python
@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")
```
``` python
test_r(cli, '/booly/?coming=true', 'Coming')
test_r(cli, '/booly/?coming=no', 'Not coming')
date_str = "17th of May, 2024, 2p"
test_r(cli, 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')
```
``` python
r = cli.get('/app2', **hxhdr)
test_eq(r.text, 'foo')
test_eq(r.headers['mykey'], 'myval')
```
``` python
r = cli.get('/app3')
test_eq(r.headers['HX-Location'], 'http://example.org')
```
``` python
r = cli.get('/app4', follow_redirects=False)
test_eq(r.status_code, 303)
```
``` python
r = cli.get('/app4', headers={'HX-Request':'1'})
test_eq(r.headers['HX-Redirect'], 'http://example.org')
```
``` python
@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
```
``` python
@app.post('/profile/me')
def profile_update(username: str): return username
test_r(cli, '/profile/me', 'Alexis', 'post', data={'username' : 'Alexis'})
test_r(cli, '/profile/me', 'Missing required field: username', 'post', data={})
```
``` python
# 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
test_r(cli, '/pet/dog', '', 'post', data={})
```
``` python
@dataclass
class Bodie: a:int;b:str
@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)
class BodieTD(TypedDict): a:int;b:str='foo'
@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}"
```
``` python
from fasthtml.xtend import Titled
```
``` python
d = dict(a=1, b='foo')
test_r(cli, '/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)
```
``` python
# Testing POST with Content-Type: application/json
@app.post("/")
def index(it: Bodie): return Titled("It worked!", P(f"{it.a}, {it.b}"))
s = json.dumps({"b": "Lorem", "a": 15})
response = cli.post('/', headers={"Content-Type": "application/json"}, data=s).text
assert "<title>It worked!</title>" in response and "<p>15, Lorem</p>" in response
```
``` python
# Testing POST with Content-Type: application/json
@app.post("/bodytext")
def index(body): return body
response = cli.post('/bodytext', headers={"Content-Type": "application/json"}, data=s).text
test_eq(response, '{"b": "Lorem", "a": 15}')
```
``` python
files = [ ('files', ('file1.txt', b'content1')),
('files', ('file2.txt', b'content2')) ]
```
``` python
@rt("/uploads")
async def post(files:list[UploadFile]):
return ','.join([(await file.read()).decode() for file in files])
res = cli.post('/uploads', files=files)
print(res.status_code)
print(res.text)
```
200
content1,content2
``` python
res = cli.post('/uploads', files=[files[0]])
print(res.status_code)
print(res.text)
```
200
content1
``` python
@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
```
Set to 2025-05-29 08:31:48.235262
'Session time: 2025-05-29 08:31:48.235262'
``` python
@rt("/sess-first")
def post(sess, name: str):
sess["name"] = name
return str(sess)
cli.post('/sess-first', data={'name': 2})
@rt("/getsess-all")
def get(sess): return sess['name']
test_eq(cli.get('/getsess-all').text, '2')
```
``` python
@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
``` python
@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)
```
``` python
h = cli.options('/form-submit/2').headers
test_eq(h['Access-Control-Allow-Methods'], 'POST')
```
``` python
from fasthtml.authmw import user_pwd_auth
```
``` python
def _not_found(req, exc): return Div('nope')
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
```
``` python
app,cli,rt = get_cli(FastHTML())
@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
```
``` python
auth = user_pwd_auth(testuser='spycraft')
app,cli,rt = get_cli(FastHTML(middleware=[auth]))
@rt("/locked")
def get(auth): return 'Hello, ' + auth
test_eq(cli.get('/locked').text, 'not authenticated')
test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser')
```
``` python
auth = user_pwd_auth(testuser='spycraft')
app,cli,rt = get_cli(FastHTML(middleware=[auth]))
@rt("/locked")
def get(auth): return 'Hello, ' + auth
test_eq(cli.get('/locked').text, 'not authenticated')
test_eq(cli.get('/locked', auth=("testuser","spycraft")).text, 'Hello, testuser')
```
## APIRouter
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L695"
target="_blank" style="float:right; font-size:smaller">source</a>
### RouteFuncs
> RouteFuncs ()
*Initialize self. See help(type(self)) for accurate signature.*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L705"
target="_blank" style="float:right; font-size:smaller">source</a>
### APIRouter
> APIRouter (prefix:str|None=None, body_wrap=<function noop_body>)
*Add routes to an app*
``` python
ar = APIRouter()
```
``` python
@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}"
```
``` python
app,cli,_ = get_cli(FastHTML())
ar.to_app(app)
```
``` python
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()
```
``` python
test_eq(cli.get('/hi').text, 'Hi there')
test_eq(cli.post('/hi').text, 'Postal')
test_eq(cli.get('/hostie').text, 'testserver')
test_eq(cli.post('/yoyo').text, 'a yoyo')
test_eq(cli.get('/ho').text, 'Ho ho')
test_eq(cli.post('/ho').text, 'Ho ho')
```
``` python
with cli.websocket_connect('/ws') as ws:
ws.send_text('{"msg":"Hi!"}')
data = ws.receive_text()
assert data == 'Message text was: Hi!'
```
``` python
ar2 = APIRouter("/products")
```
``` python
@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}"
```
``` python
app,cli,_ = get_cli(FastHTML())
ar2.to_app(app)
```
``` python
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()
```
``` python
test_eq(cli.get('/products/hi').text, 'Hi there')
test_eq(cli.post('/products/hi').text, 'Postal')
test_eq(cli.get('/products/hostie').text, 'testserver')
test_eq(cli.post('/products/yoyo').text, 'a yoyo')
test_eq(cli.get('/products/ho').text, 'Ho ho')
test_eq(cli.post('/products/ho').text, 'Ho ho')
```
``` python
with cli.websocket_connect('/products/ws') as ws:
ws.send_text('{"msg":"Hi!"}')
data = ws.receive_text()
assert data == 'Message text was: Hi!'
```
``` python
@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
``` python
app,cli,rt = get_cli(FastHTML(secret_key='soopersecret'))
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L748"
target="_blank" style="float:right; font-size:smaller">source</a>
### cookie
> cookie (key:str, value='', max_age=None, expires=None, path='/',
> domain=None, secure=False, httponly=False, samesite='lax')
*Create a ‘set-cookie’
[`HttpHeader`](https://www.fastht.ml/docs/api/core.html#httpheader)*
``` python
@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
```
<!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="http://testserver/setcookie">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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></body>
</html>
'Cookie was set at time 08:31:49.013668'
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L766"
target="_blank" style="float:right; font-size:smaller">source</a>
### reg_re_param
> reg_re_param (m, s)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L777"
target="_blank" style="float:right; font-size:smaller">source</a>
### 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()`](https://www.fastht.ml/docs/api/core.html#reg_re_param)*
``` python
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}"
test_r(cli, '/static/foo/jph.me.ico', 'Getting jph.me.ico from /foo/')
```
``` python
app.static_route_exts()
assert 'These are the source notebooks for FastHTML' in cli.get('/README.txt').text
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L784"
target="_blank" style="float:right; font-size:smaller">source</a>
### 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 ‘.’)*
``` python
app.static_route('.md', static_path='../..')
assert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text
```
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L790"
target="_blank" style="float:right; font-size:smaller">source</a>
### MiddlewareBase
> MiddlewareBase ()
*Initialize self. See help(type(self)) for accurate signature.*
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L798"
target="_blank" style="float:right; font-size:smaller">source</a>
### FtResponse
> FtResponse (content, status_code:int=200, headers=None, cls=<class
> 'starlette.responses.HTMLResponse'>,
> media_type:str|None=None,
> background:starlette.background.BackgroundTask|None=None)
*Wrap an FT response with any Starlette `Response`*
``` python
@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
```
Test on a single background task:
``` python
def my_slow_task():
print('Starting slow task')
time.sleep(0.001)
print('Finished slow task')
@rt('/background')
def get():
return P('BG Task'), BackgroundTask(my_slow_task)
r = cli.get('/background')
test_eq(r.status_code, 200)
```
Starting slow task
Finished slow task
Test multiple background tasks:
``` python
def increment(amount):
amount = amount/1000
print(f'Sleeping for {amount}s')
time.sleep(amount)
print(f'Slept for {amount}s')
```
``` python
@rt
def backgrounds():
tasks = BackgroundTasks()
for i in range(3): tasks.add_task(increment, i)
return P('BG Tasks'), tasks
r = cli.get('/backgrounds')
test_eq(r.status_code, 200)
```
Sleeping for 0.0s
Slept for 0.0s
Sleeping for 0.001s
Slept for 0.001s
Sleeping for 0.002s
Slept for 0.002s
``` python
@rt
def backgrounds2():
tasks = [BackgroundTask(increment,i) for i in range(3)]
return P('BG Tasks'), *tasks
r = cli.get('/backgrounds2')
test_eq(r.status_code, 200)
```
Sleeping for 0.0s
Slept for 0.0s
Sleeping for 0.001s
Slept for 0.001s
Sleeping for 0.002s
Slept for 0.002s
``` python
@rt
def backgrounds3():
tasks = [BackgroundTask(increment,i) for i in range(3)]
return {'status':'done'}, *tasks
r = cli.get('/backgrounds3')
test_eq(r.status_code, 200)
r.json()
```
Sleeping for 0.0s
Slept for 0.0s
Sleeping for 0.001s
Slept for 0.001s
Sleeping for 0.002s
Slept for 0.002s
{'status': 'done'}
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L813"
target="_blank" style="float:right; font-size:smaller">source</a>
### unqid
> unqid (seeded=False)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L825"
target="_blank" style="float:right; font-size:smaller">source</a>
### setup_ws
> setup_ws (app, f=<function noop>)
------------------------------------------------------------------------
<a
href="https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/core.py#L839"
target="_blank" style="float:right; font-size:smaller">source</a>
### FastHTML.devtools_json
> FastHTML.devtools_json (path=None, uuid=None)