Spaces:
Sleeping
Sleeping
| # 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/htmx.org@2.0.4/dist/htmx.min.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/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) | |