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(2024, 11, 4, 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()
'77486da1-c613-48be-80c4-9cae89eeec48'
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, 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 application instance.
Parameters:
- debug - Boolean indicating if debug tracebacks should be returned on errors.
- routes - A list of routes to serve incoming HTTP and WebSocket requests.
- middleware - A list of middleware to run for every request. A starlette application will always automatically include two middleware classes.
ServerErrorMiddleware
is added as the very outermost middleware, to handle any uncaught errors occurring anywhere in the entire stack.ExceptionMiddleware
is added as the very innermost middleware, to deal with handled exception cases occurring in the routing or endpoints. - exception_handlers - A mapping of either integer status codes, or exception class types onto callables which handle the exceptions. Exception handler callables should be of the form
handler(request, exc) -> response
and may be either standard functions, or async functions. - on_startup - A list of callables to run on application startup. Startup handler callables do not take any arguments, and may be either standard functions, or async functions.
- on_shutdown - A list of callables to run on application shutdown. Shutdown handler callables do not take any arguments, and may be either standard functions, or async functions.
- lifespan - A lifespan context function, which can be used to perform startup and shutdown tasks. This is a newer style that replaces the
on_startup
andon_shutdown
handlers. Use one or the other, not both.*
FastHTML.ws
FastHTML.ws (path:str, conn=None, disconn=None, name=None, middleware=None)
Add a websocket route at path
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
@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 2024-11-04 15:30:23.038930
'Session time: 2024-11-04 15:30:23.038930'
@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
APIRouter
APIRouter ()
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.ws("/ws")
def ws(self, msg:str): return f"Message text was: {msg}"
= get_cli(FastHTML())
app,cli,_ ar.to_app(app)
'/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!'
@ar.get
def hi2(): return 'Hi there'
@ar.get("/hi3")
def _(): return 'Hi there'
@ar.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")
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>)