# Core 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. ------------------------------------------------------------------------ source ### 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 ------------------------------------------------------------------------ source ### snake2hyphens > snake2hyphens (s:str) *Convert `s` from snake case to hyphenated and capitalised* ``` python 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) ``` 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]) ``` ------------------------------------------------------------------------ source ### HttpHeader > HttpHeader (k:str, v:str) ``` python _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* ``` python 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* ``` 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) ``` ------------------------------------------------------------------------ source ### parse_form > parse_form (req:starlette.requests.Request) *Starlette errors on empty multipart forms, so this checks for that situation* ------------------------------------------------------------------------ source ### 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) ``` [, , '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) ``` [, , '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) ``` [, , ''] 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 ------------------------------------------------------------------------ source ### 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,)) ``` ------------------------------------------------------------------------ source ### 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!' ``` ------------------------------------------------------------------------ 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* Render fragment if `HX-Request` header is *present* and `HX-History-Restore-Request` header is *absent.* ------------------------------------------------------------------------ source ### is_full_page > is_full_page (req, resp) ------------------------------------------------------------------------ source ### 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 ------------------------------------------------------------------------ source ### get_key > get_key (key=None, fname='.sesskey') ------------------------------------------------------------------------ source ### 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=') ``` ------------------------------------------------------------------------ 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= '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= noop_body>, htmlkw=None, nb_hdrs=False, canonical=True, > **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\* ``` python def f(): def g(): ... return g ``` ``` python 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`* ``` 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' ------------------------------------------------------------------------ source ### 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
------------------------------------------------------------------------ source ### 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 'My Custom 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 'FastHTML page' in txt and '

bar

' in txt and '' 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) ```
Text.
``` 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) ```
Text.
``` python @app.get def autoget2(): return Html(Div('Text.', hx_get=show_host)) print(cli.get('/autoget2').text) ```
Text.
``` 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) ```
Text.
``` 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 'Foo' in txt and '

bar

' in txt and '' in txt @rt('/xt2') def get(): return H1('bar') txt = cli.get('/xt2').text assert 'FastHTML page' in txt and '

bar

' in txt and '' in txt assert cli.get('/xt2', **hxhdr).text.strip() == '

bar

' @rt('/xt3') def get(): return Html(Head(Title('hi')), Body(P('there'))) txt = cli.get('/xt3').text assert 'FastHTML page' not in txt and 'hi' in txt and '

there

' 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 '

Next is 2.

' 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'\s*

hi

\s*', t) assert 'It worked!" in response and "

15, Lorem

" 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 '
nope
' in txt assert '' 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 'Hello Uma, age 5' 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 ------------------------------------------------------------------------ source ### RouteFuncs > RouteFuncs () *Initialize self. See help(type(self)) for accurate signature.* ------------------------------------------------------------------------ source ### APIRouter > APIRouter (prefix:str|None=None, body_wrap=) *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')) ``` ------------------------------------------------------------------------ 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`](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 ``` FastHTML page 'Cookie was set at time 08:31:49.013668' ------------------------------------------------------------------------ source ### 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()`](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 ``` ------------------------------------------------------------------------ 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 ‘.’)* ``` python 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= '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 'Foo' in txt and '

bar

' in txt and '' 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'} ------------------------------------------------------------------------ source ### unqid > unqid (seeded=False) ------------------------------------------------------------------------ source ### setup_ws > setup_ws (app, f=) ------------------------------------------------------------------------ source ### FastHTML.devtools_json > FastHTML.devtools_json (path=None, uuid=None)