diff --git a/fasthtml/__init__.py b/fasthtml/__init__.py
index 88cf9950..17d1f1e2 100644
--- a/fasthtml/__init__.py
+++ b/fasthtml/__init__.py
@@ -1,2 +1,8 @@
+"""The fastest way to create an HTML app
+
+Modules:
+
+- `fasthtml.core`: The `FastHTML` subclass of `Starlette`."""
+
__version__ = "0.14.12"
from .core import *
diff --git a/fasthtml/core.py b/fasthtml/core.py
index 46b4d1b9..792f4d56 100644
--- a/fasthtml/core.py
+++ b/fasthtml/core.py
@@ -1,5 +1,45 @@
"""The `FastHTML` subclass of `Starlette`.
+Create an app with `app = FastHTML()`. The `@rt` decorator adds routes (`rt = app.route`), inferring what it can from the function:
+
+ @rt
+ def index(): ... # GET,POST /
+ @rt
+ def foo(): ... # GET,POST /foo
+ @rt('/hi')
+ def get(): ... # GET /hi (function named after a verb handles just that verb)
+ @rt('/cards/{id}')
+ class Cards: ... # class route group: each verb-named method (get, patch, ...) becomes a handler
+
+The decorated function doubles as a URL builder, so links never need hardcoded paths:
+
+ A('More', href=foo.to(a=1)) # `.to()` fills path params; the rest become query params.
+ Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get="/foo"
+
+Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in ``; the rest goes in ``. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.
+
+Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation:
+
+ req, ws the Request / WebSocket connection
+ sess session dict
+ app the FastHTML app
+ state app.state
+ scope ASGI scope
+ auth scope['auth']
+ htmx HtmxHeaders (parsed HX-* request headers)
+ body raw body text
+ data parsed form data as a dict
+ api ApiReturn (API vs browser dual responses)
+ send websocket send function
+ hdrs,ftrs the per-request copies of the app's `hdrs`/`ftrs` (head and footer content)
+ htmlkw the per-request copy of the app's `` attrs
+ bodykw the per-request copy of the app's `` attrs
+ resp the handler's response (injected into `after` functions)
+
+`before`/`after` functions (app-level, with `Beforeware` skip patterns, or per-route `before=`) get the same param injection as handlers. A `before` returning a value responds with it and skips the handler (the usual auth-guard pattern).
+
+See the [Handling Handlers tutorial](https://www.fastht.ml/docs/ref/handlers.html.md) for worked examples of the above.
+
Docs: https://www.fastht.ml/docs/api/core.html.md"""
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.
@@ -72,6 +112,7 @@ def snake2hyphens(s:str):
@dataclass
class HtmxHeaders:
+ "Parsed HX-* request headers"
boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None
request:str|None=None; request_type:str|None=None; source:str|None=None; target:str|None=None;
trigger_name:str|None=None; trigger:str|None=None
@@ -120,7 +161,9 @@ def _form_arg(k, v, d):
# %% ../nbs/api/00_core.ipynb #5fc04751
@dataclass
-class HttpHeader: k:str;v:str
+class HttpHeader:
+ "A response header as a k,v pair"
+ k:str;v:str
# %% ../nbs/api/00_core.ipynb #94e18161
def _to_htmx_header(s): return 'HX-' + s.replace('_', '-').title()
@@ -191,6 +234,7 @@ async def _from_body(conn, p, data):
# %% ../nbs/api/00_core.ipynb #88b6da3f
class ApiReturn:
+ "Request param: call with browser and API responses; returns kwargs as JSON if the client accepts `application/json`, else `norm`"
@classmethod
async def __from_request__(cls, data, req): return cls(req.headers.get('accept')=='application/json')
def __init__(self, isapi=False): self.isapi = isapi
@@ -278,6 +322,7 @@ def flat_xt(lst):
# %% ../nbs/api/00_core.ipynb #aacff5ac
class Beforeware:
+ "Wrap a `before` function with `skip` path patterns that bypass it"
def __init__(self, f, skip=None): self.f,self.skip = f,skip or []
def __repr__(self): return f'Beforeware({self.f}, skip={self.skip})'
@@ -326,6 +371,7 @@ def EventStream(s):
# %% ../nbs/api/00_core.ipynb #0dd0a414
def signal_shutdown():
+ "Return an `asyncio.Event` set when uvicorn receives a shutdown signal"
from uvicorn.main import Server
event = asyncio.Event()
@patch
@@ -614,6 +660,7 @@ def __aiter__(self): return self.gen
async def __anext__(self): return await self.gen.__anext__()
class Lifespan:
+ "Combine `on_startup`/`on_shutdown` callbacks and an optional lifespan generator into one context manager"
def __init__(self, startup=None, shutdown=None, ls=None):
startup,shutdown = listify(startup),listify(shutdown)
store_attr()
@@ -632,6 +679,7 @@ def on_event(self, event_type):
# %% ../nbs/api/00_core.ipynb #3327a1e9
class FastHTML(Starlette):
+ "An HTML-first Starlette app: handler params filled from the request, FT returns rendered as pages or HTMX fragments"
def __init__(self, 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, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware,
@@ -895,6 +943,7 @@ async def _request(): return await self.cli.request(method, url, **kwargs)
# %% ../nbs/api/00_core.ipynb #d5223a9a
class RouteFuncs:
+ "Attr-access store of named route functions (HTTP verb names excluded)"
def __init__(self): super().__setattr__('_funcs', {})
def __setattr__(self, name, value): self._funcs[name] = value
def __getattr__(self, name):
@@ -966,6 +1015,7 @@ def cookie(key: str, value="", max_age=None, expires=None, path="/", domain=None
# %% ../nbs/api/00_core.ipynb #8816f277
def reg_re_param(m, s):
+ "Register a Starlette URL convertor named `m` matching regex `s`"
cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)
register_url_convertor(m, cls())
@@ -990,6 +1040,7 @@ async def get(fname:str): return FileResponse(f'{static_path}/{fname}{ext}')
# %% ../nbs/api/00_core.ipynb #f63b7a03
class StaticNoCache(StaticFiles):
+ "StaticFiles that sends `Cache-Control: no-cache`, so browsers revalidate on every use"
def file_response(self, *args, **kwargs):
resp = super().file_response(*args, **kwargs)
resp.headers.setdefault("Cache-Control", "no-cache")
@@ -1054,11 +1105,11 @@ async def _inner(*args, **kw):
# %% ../nbs/api/00_core.ipynb #1960d7ff
class MiddlewareBase:
+ "Base for middleware: passes non-http/ws scopes through, else returns the `HTTPConnection`"
async def __call__(self, scope, receive, send) -> None:
- if scope["type"] not in ["http", "websocket"]:
- await self._app(scope, receive, send)
- return
- return HTTPConnection(scope)
+ if scope["type"] in ("http", "websocket"): return HTTPConnection(scope)
+ await self._app(scope, receive, send)
+
# %% ../nbs/api/00_core.ipynb #83a20f93
class FtResponse:
@@ -1077,6 +1128,7 @@ def __response__(self, req):
# %% ../nbs/api/00_core.ipynb #9dc1025e
def unqid(seeded=False):
+ "Random unique id, base64-encoded and prefixed with '_' so it's usable as an HTML id"
id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4()
res = b64encode(id4.bytes)
return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-'))
diff --git a/nbs/api/00_core.ipynb b/nbs/api/00_core.ipynb
index d8827e07..a64df81e 100644
--- a/nbs/api/00_core.ipynb
+++ b/nbs/api/00_core.ipynb
@@ -19,6 +19,53 @@
"> The `FastHTML` subclass of `Starlette`."
]
},
+ {
+ "cell_type": "markdown",
+ "id": "1f8f308f",
+ "metadata": {},
+ "source": [
+ "#| export\n",
+ "Create an app with `app = FastHTML()`. The `@rt` decorator adds routes (`rt = app.route`), inferring what it can from the function:\n",
+ "\n",
+ " @rt\n",
+ " def index(): ... # GET,POST /\n",
+ " @rt\n",
+ " def foo(): ... # GET,POST /foo\n",
+ " @rt('/hi')\n",
+ " def get(): ... # GET /hi (function named after a verb handles just that verb)\n",
+ " @rt('/cards/{id}')\n",
+ " class Cards: ... # class route group: each verb-named method (get, patch, ...) becomes a handler\n",
+ "\n",
+ "The decorated function doubles as a URL builder, so links never need hardcoded paths:\n",
+ "\n",
+ " A('More', href=foo.to(a=1)) # `.to()` fills path params; the rest become query params.\n",
+ " Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get=\"/foo\"\n",
+ "\n",
+ "Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in ``; the rest goes in ``. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.\n",
+ "\n",
+ "Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation:\n",
+ "\n",
+ " req, ws the Request / WebSocket connection\n",
+ " sess session dict\n",
+ " app the FastHTML app\n",
+ " state app.state\n",
+ " scope ASGI scope\n",
+ " auth scope['auth']\n",
+ " htmx HtmxHeaders (parsed HX-* request headers)\n",
+ " body raw body text\n",
+ " data parsed form data as a dict\n",
+ " api ApiReturn (API vs browser dual responses)\n",
+ " send websocket send function\n",
+ " hdrs,ftrs the per-request copies of the app's `hdrs`/`ftrs` (head and footer content)\n",
+ " htmlkw the per-request copy of the app's `` attrs\n",
+ " bodykw the per-request copy of the app's `` attrs\n",
+ " resp the handler's response (injected into `after` functions)\n",
+ "\n",
+ "`before`/`after` functions (app-level, with `Beforeware` skip patterns, or per-route `before=`) get the same param injection as handlers. A `before` returning a value responds with it and skips the handler (the usual auth-guard pattern).\n",
+ "\n",
+ "See the [Handling Handlers tutorial](https://www.fastht.ml/docs/ref/handlers.html.md) for worked examples of the above."
+ ]
+ },
{
"cell_type": "markdown",
"id": "46e2e6e8",
@@ -224,6 +271,7 @@
"\n",
"@dataclass\n",
"class HtmxHeaders:\n",
+ " \"Parsed HX-* request headers\"\n",
" boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None\n",
" request:str|None=None; request_type:str|None=None; source:str|None=None; target:str|None=None; \n",
" trigger_name:str|None=None; trigger:str|None=None\n",
@@ -404,7 +452,9 @@
"source": [
"#| export\n",
"@dataclass\n",
- "class HttpHeader: k:str;v:str"
+ "class HttpHeader:\n",
+ " \"A response header as a k,v pair\"\n",
+ " k:str;v:str"
]
},
{
@@ -692,6 +742,7 @@
"source": [
"#| export\n",
"class ApiReturn:\n",
+ " \"Request param: call with browser and API responses; returns kwargs as JSON if the client accepts `application/json`, else `norm`\"\n",
" @classmethod\n",
" async def __from_request__(cls, data, req): return cls(req.headers.get('accept')=='application/json')\n",
" def __init__(self, isapi=False): self.isapi = isapi\n",
@@ -1126,6 +1177,7 @@
"source": [
"#| export\n",
"class Beforeware:\n",
+ " \"Wrap a `before` function with `skip` path patterns that bypass it\"\n",
" def __init__(self, f, skip=None): self.f,self.skip = f,skip or []\n",
" def __repr__(self): return f'Beforeware({self.f}, skip={self.skip})'"
]
@@ -1264,6 +1316,7 @@
"source": [
"#| export\n",
"def signal_shutdown():\n",
+ " \"Return an `asyncio.Event` set when uvicorn receives a shutdown signal\"\n",
" from uvicorn.main import Server\n",
" event = asyncio.Event()\n",
" @patch\n",
@@ -1851,6 +1904,7 @@
" async def __anext__(self): return await self.gen.__anext__()\n",
"\n",
"class Lifespan:\n",
+ " \"Combine `on_startup`/`on_shutdown` callbacks and an optional lifespan generator into one context manager\"\n",
" def __init__(self, startup=None, shutdown=None, ls=None):\n",
" startup,shutdown = listify(startup),listify(shutdown)\n",
" store_attr()\n",
@@ -1877,6 +1931,7 @@
"source": [
"#| export\n",
"class FastHTML(Starlette):\n",
+ " \"An HTML-first Starlette app: handler params filled from the request, FT returns rendered as pages or HTMX fragments\"\n",
" def __init__(self, debug=False, routes=None, middleware=None, title: str = \"FastHTML page\", exception_handlers=None,\n",
" on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None,\n",
" before=None, after=None, surreal=True, htmx=True, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware,\n",
@@ -3936,6 +3991,7 @@
"source": [
"#| export\n",
"class RouteFuncs:\n",
+ " \"Attr-access store of named route functions (HTTP verb names excluded)\"\n",
" def __init__(self): super().__setattr__('_funcs', {})\n",
" def __setattr__(self, name, value): self._funcs[name] = value\n",
" def __getattr__(self, name):\n",
@@ -4332,6 +4388,7 @@
"source": [
"#| export\n",
"def reg_re_param(m, s):\n",
+ " \"Register a Starlette URL convertor named `m` matching regex `s`\"\n",
" cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)\n",
" register_url_convertor(m, cls())"
]
@@ -4417,6 +4474,7 @@
"source": [
"#| export\n",
"class StaticNoCache(StaticFiles):\n",
+ " \"StaticFiles that sends `Cache-Control: no-cache`, so browsers revalidate on every use\"\n",
" def file_response(self, *args, **kwargs):\n",
" resp = super().file_response(*args, **kwargs)\n",
" resp.headers.setdefault(\"Cache-Control\", \"no-cache\")\n",
@@ -4651,11 +4709,11 @@
"source": [
"#| export\n",
"class MiddlewareBase:\n",
+ " \"Base for middleware: passes non-http/ws scopes through, else returns the `HTTPConnection`\"\n",
" async def __call__(self, scope, receive, send) -> None:\n",
- " if scope[\"type\"] not in [\"http\", \"websocket\"]:\n",
- " await self._app(scope, receive, send)\n",
- " return\n",
- " return HTTPConnection(scope)"
+ " if scope[\"type\"] in (\"http\", \"websocket\"): return HTTPConnection(scope)\n",
+ " await self._app(scope, receive, send)\n",
+ " "
]
},
{
@@ -4966,6 +5024,7 @@
"source": [
"#| export\n",
"def unqid(seeded=False):\n",
+ " \"Random unique id, base64-encoded and prefixed with '_' so it's usable as an HTML id\"\n",
" id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4()\n",
" res = b64encode(id4.bytes)\n",
" return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-'))"
@@ -5120,6 +5179,13 @@
"display_name": "python3",
"language": "python",
"name": "python3"
+ },
+ "solveit": {
+ "default_code": true,
+ "mode": "learning",
+ "use_thinking": true,
+ "use_tools": true,
+ "ver": 2
}
},
"nbformat": 4,