├── nbs ├── .gitignore ├── api │ ├── README.txt │ └── 09_cli.ipynb ├── favicon.ico ├── og-image.png ├── robots.txt ├── explains │ ├── refund.png │ ├── CreateWebhook.png │ ├── CreateWebhook2.png │ ├── imgs │ │ └── gh-oauth.png │ ├── StripePaymentPage.jpg │ ├── SubscriptionEvents.png │ ├── StripeDashboard_API_Key.png │ ├── faq.ipynb │ ├── websockets.ipynb │ ├── stripe_otp.py │ ├── background_tasks.ipynb │ └── routes.ipynb ├── fonts │ ├── Geist-Bold.woff │ ├── Geist-Bold.woff2 │ ├── Geist-Regular.woff │ ├── Geist-Regular.woff2 │ ├── GeistMono-Bold.woff │ ├── GeistMono-Bold.woff2 │ ├── GeistMono-Regular.woff │ └── GeistMono-Regular.woff2 ├── tutorials │ ├── imgs │ │ └── quickdraw.png │ ├── quickstart-web-dev │ │ ├── quickstart-charts.png │ │ ├── quickstart-fasthtml.png │ │ └── quickstart-sakura.png │ ├── hello_world.py │ ├── index.qmd │ ├── 01_quickdraw.py │ ├── 02_quickdraw.py │ ├── 03_quickdraw.py │ └── jupyter_and_fasthtml.ipynb ├── unpublished │ └── web-dev-tut │ │ └── random-list-letters.png ├── nbdev.yml ├── custom.scss ├── fix-commonmark.html ├── styles.css ├── _quarto.yml ├── ref │ ├── live_reload.ipynb │ ├── best_practice.qmd │ └── response_types.ipynb ├── llms.txt └── index.ipynb ├── demo ├── requirements.txt ├── favicon.ico └── main.py ├── examples ├── example.js ├── favicon.ico ├── picovars.css ├── minimal.py ├── README.md ├── chat_ws.py ├── basic_ws.py ├── todos3.py ├── todos1.py ├── todos4.py ├── todos2.py ├── user_app.py ├── adv_app_strip.py └── pep8_app.py ├── fasthtml ├── __init__.py ├── basics.py ├── ft.py ├── common.py ├── katex.js ├── starlette.py ├── cli.py ├── authmw.py ├── live_reload.py ├── toaster.py ├── pico.py ├── fastapp.py ├── js.py ├── jupyter.py └── svg.py ├── mds.zip ├── MANIFEST.in ├── .github ├── workflows │ ├── test.yaml │ └── deploy.yaml.off ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ ├── bug_report.md │ └── documentation.md └── pull_request_template.md ├── tools ├── update.sh └── mk_pyi.py ├── docs └── index.html ├── .codespellrc ├── pyproject.toml ├── settings.ini ├── tests └── test_toaster.py ├── .gitignore ├── setup.py ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── README.md /nbs/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | python-fasthtml -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | console.log(1) 2 | -------------------------------------------------------------------------------- /fasthtml/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.12.37" 2 | from .core import * 3 | -------------------------------------------------------------------------------- /mds.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/mds.zip -------------------------------------------------------------------------------- /nbs/api/README.txt: -------------------------------------------------------------------------------- 1 | These are the source notebooks for FastHTML. 2 | 3 | -------------------------------------------------------------------------------- /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/demo/favicon.ico -------------------------------------------------------------------------------- /nbs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/favicon.ico -------------------------------------------------------------------------------- /nbs/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/og-image.png -------------------------------------------------------------------------------- /examples/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/examples/favicon.ico -------------------------------------------------------------------------------- /nbs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Sitemap: https://www.fastht.ml/docs/sitemap.xml 4 | -------------------------------------------------------------------------------- /fasthtml/basics.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | from .components import * 3 | from .xtend import * 4 | 5 | -------------------------------------------------------------------------------- /nbs/explains/refund.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/refund.png -------------------------------------------------------------------------------- /fasthtml/ft.py: -------------------------------------------------------------------------------- 1 | from fastcore.xml import * 2 | from .components import * 3 | from .xtend import * 4 | 5 | -------------------------------------------------------------------------------- /nbs/fonts/Geist-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/Geist-Bold.woff -------------------------------------------------------------------------------- /nbs/fonts/Geist-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/Geist-Bold.woff2 -------------------------------------------------------------------------------- /nbs/fonts/Geist-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/Geist-Regular.woff -------------------------------------------------------------------------------- /examples/picovars.css: -------------------------------------------------------------------------------- 1 | /* Used in demonstrating how to link to a css file */ 2 | :root { --pico-font-size: 100%; } 3 | -------------------------------------------------------------------------------- /nbs/explains/CreateWebhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/CreateWebhook.png -------------------------------------------------------------------------------- /nbs/explains/CreateWebhook2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/CreateWebhook2.png -------------------------------------------------------------------------------- /nbs/explains/imgs/gh-oauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/imgs/gh-oauth.png -------------------------------------------------------------------------------- /nbs/fonts/Geist-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/Geist-Regular.woff2 -------------------------------------------------------------------------------- /nbs/fonts/GeistMono-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/GeistMono-Bold.woff -------------------------------------------------------------------------------- /nbs/fonts/GeistMono-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/GeistMono-Bold.woff2 -------------------------------------------------------------------------------- /nbs/fonts/GeistMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/GeistMono-Regular.woff -------------------------------------------------------------------------------- /nbs/fonts/GeistMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/fonts/GeistMono-Regular.woff2 -------------------------------------------------------------------------------- /nbs/tutorials/imgs/quickdraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/tutorials/imgs/quickdraw.png -------------------------------------------------------------------------------- /nbs/explains/StripePaymentPage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/StripePaymentPage.jpg -------------------------------------------------------------------------------- /nbs/explains/SubscriptionEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/SubscriptionEvents.png -------------------------------------------------------------------------------- /nbs/explains/StripeDashboard_API_Key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/explains/StripeDashboard_API_Key.png -------------------------------------------------------------------------------- /nbs/unpublished/web-dev-tut/random-list-letters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/unpublished/web-dev-tut/random-list-letters.png -------------------------------------------------------------------------------- /nbs/tutorials/quickstart-web-dev/quickstart-charts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/tutorials/quickstart-web-dev/quickstart-charts.png -------------------------------------------------------------------------------- /nbs/tutorials/quickstart-web-dev/quickstart-fasthtml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/tutorials/quickstart-web-dev/quickstart-fasthtml.png -------------------------------------------------------------------------------- /nbs/tutorials/quickstart-web-dev/quickstart-sakura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnswerDotAI/fasthtml/HEAD/nbs/tutorials/quickstart-web-dev/quickstart-sakura.png -------------------------------------------------------------------------------- /nbs/tutorials/hello_world.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app = FastHTML() 4 | rt = app.route 5 | 6 | @rt('/') 7 | def get(): 8 | return 'Hello, world!' 9 | 10 | serve() -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include fasthtml/*.pyi 2 | include fasthtml/*.js 3 | include settings.ini 4 | include LICENSE 5 | include CONTRIBUTING.md 6 | include README.md 7 | recursive-exclude * __pycache__ 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [workflow_dispatch, pull_request, push] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: [uses: fastai/workflows/nbdev-ci@master] 8 | -------------------------------------------------------------------------------- /tools/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | python tools/mk_pyi.py 3 | llms_txt2ctx nbs/llms.txt --optional true > nbs/llms-ctx-full.txt 4 | llms_txt2ctx nbs/llms.txt > nbs/llms-ctx.txt 5 | pysym2md --output_file nbs/apilist.txt fasthtml --include_no_docstring 6 | 7 | -------------------------------------------------------------------------------- /nbs/nbdev.yml: -------------------------------------------------------------------------------- 1 | project: 2 | output-dir: _docs 3 | 4 | website: 5 | title: "fasthtml" 6 | site-url: "https://www.fastht.ml/docs/" 7 | description: "The fastest way to create an HTML app" 8 | repo-branch: main 9 | repo-url: "https://github.com/AnswerDotAI/fasthtml" 10 | -------------------------------------------------------------------------------- /nbs/tutorials/index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | title: Tutorials 4 | listing: 5 | fields: [title, description] 6 | type: table 7 | sort-ui: false 8 | filter-ui: false 9 | --- 10 | 11 | Click through to any of these tutorials to get started with FastHTML's features. 12 | 13 | -------------------------------------------------------------------------------- /examples/minimal.py: -------------------------------------------------------------------------------- 1 | from random import random 2 | from fasthtml.common import * 3 | app,rt = fast_app( hdrs=[Script(src='example.js')]) 4 | 5 | @rt 6 | def rnd(): return P(random()) 7 | 8 | @rt 9 | def index(): return Titled( 'Hello', Div(P('click'), hx_post=rnd)) 10 | 11 | serve() 12 | 13 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |If you are not redirected automatically, click here.
9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Start a discussion 4 | url: https://github.com/AnswerDotAI/fasthtml/discussions 5 | about: You can ask for help here! 6 | - name: Join our Discord 7 | url: https://discord.gg/qcXvcxMhdP 8 | about: Join our community on Discord! 9 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml.off: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | permissions: 3 | contents: write 4 | pages: write 5 | 6 | on: 7 | push: 8 | branches: [ "main", "master" ] 9 | workflow_dispatch: 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: [uses: fastai/workflows/quarto-ghp@master] 14 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains some examples of the library in action, including a sequence of to-do list apps of increasing complexity (see `todos1.py` through `todos5.py`, along with `adv_app.py`). For more examples of different applications and components, see the separate [examples repository](https://github.com/AnswerDotAI/fasthtml-example). 4 | 5 | -------------------------------------------------------------------------------- /fasthtml/common.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from dataclasses import dataclass 3 | from typing import Any 4 | 5 | from .starlette import * 6 | from fastcore.utils import * 7 | from fastcore.xml import * 8 | from apswutils import Database 9 | from fastlite import * 10 | from .basics import * 11 | from .pico import * 12 | from .authmw import * 13 | from .live_reload import * 14 | from .toaster import * 15 | from .js import * 16 | from .fastapp import * 17 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | # See: https://github.com/codespell-project/codespell#using-a-config-file 2 | [codespell] 3 | # In the event of a false positive, add the problematic word, in all lowercase, to a comma-separated list here: 4 | ignore-words-list = convertor,convertors,noo,socio-economic 5 | # If a file has many false positives, exclude here: 6 | skip = ./.git 7 | # To exclude base64 encoded strings, ignore all long base64 compatible strings 8 | ignore-regex = [A-Za-z0-9+/]{1000,} 9 | -------------------------------------------------------------------------------- /nbs/custom.scss: -------------------------------------------------------------------------------- 1 | /*-- scss:defaults --*/ 2 | 3 | $body-bg: #FFFFFF !default; 4 | $body-color: #000000 !default; 5 | $link-color: #4040BF !default; 6 | $primary: #E8E8FC !default; 7 | 8 | $code-bg: null !default; // Placeholder for default value 9 | $code-color: null !default; // Placeholder for default value 10 | 11 | $navbar-bg: #3CDD8C !default; 12 | $navbar-fg: #FFFFFF !default; 13 | $navbar-hl: #000000 !default; 14 | 15 | $font-size-root: 18px !default; 16 | 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name="python-fasthtml" 7 | requires-python=">=3.10" 8 | dynamic = [ "keywords", "description", "version", "dependencies", "optional-dependencies", "readme", "license", "authors", "classifiers", "entry-points", "scripts", "urls"] 9 | 10 | [tool.uv] 11 | cache-keys = [{ file = "pyproject.toml" }, { file = "settings.ini" }, { file = "setup.py" }] 12 | -------------------------------------------------------------------------------- /examples/chat_ws.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app = FastHTML(exts='ws') 4 | rt = app.route 5 | 6 | msgs = [] 7 | @rt('/') 8 | def home(): 9 | return Div(hx_ext='ws', ws_connect='/ws')( 10 | Div(Ul(*[Li(m) for m in msgs], id='msg-list')), 11 | Form(Input(id='msg'), id='form', ws_send=True) 12 | ) 13 | 14 | async def ws(msg:str): 15 | msgs.append(msg) 16 | await send(Ul(*[Li(m) for m in msgs], id='msg-list')) 17 | 18 | send = app.setup_ws(ws) 19 | 20 | serve() 21 | -------------------------------------------------------------------------------- /nbs/fix-commonmark.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/basic_ws.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from fasthtml.common import * 3 | 4 | app = FastHTML(exts='ws') 5 | rt = app.route 6 | 7 | def mk_inp(): return Input(id='msg') 8 | nid = 'notifications' 9 | 10 | @rt('/') 11 | async def get(): 12 | cts = Div( 13 | Div(id=nid), 14 | Form(mk_inp(), id='form', ws_send=True), 15 | hx_ext='ws', ws_connect='/ws') 16 | return Titled('Websocket Test', cts) 17 | 18 | async def on_connect(send): await send(Div('Hello, you have connected', id=nid)) 19 | async def on_disconnect( ): print('Disconnected!') 20 | 21 | @app.ws('/ws', conn=on_connect, disconn=on_disconnect) 22 | async def ws(msg:str, send): 23 | await send(Div('Hello ' + msg, id=nid)) 24 | await sleep(2) 25 | return Div('Goodbye ' + msg, id=nid), mk_inp() 26 | 27 | serve() 28 | 29 | -------------------------------------------------------------------------------- /tools/mk_pyi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from fasthtml.common import * 3 | from fasthtml.components import _all_, hx_attrs_annotations 4 | from fastcore.py2pyi import create_pyi 5 | 6 | create_pyi('fasthtml/core.py', 'fasthtml') 7 | create_pyi('fasthtml/components.py', 'fasthtml') 8 | create_pyi('fasthtml/xtend.py', 'fasthtml') 9 | with open('fasthtml/components.pyi', 'a') as f: 10 | attrs_str = ', '.join(f'{t}:Any=None' for t in hx_attrs) 11 | f.write(f"\ndef ft_html(tag: str, *c, {attrs_str}, **kwargs): ...\n") 12 | f.write(f"def ft_hx(tag: str, *c, {attrs_str}, **kwargs): ...\n") 13 | for o in _all_: 14 | attrs = (['name'] if o.lower() in named else []) + hx_attrs + evt_attrs 15 | attrs_str = ', '.join(f'{t}:{"Any" if t not in hx_attrs_annotations else str(hx_attrs_annotations[t]).replace("typing.","")}=None' for t in attrs) 16 | f.write(f"def {o}(*c, {attrs_str}, **kwargs): ...\n") 17 | -------------------------------------------------------------------------------- /demo/main.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | def render(item:'Todo'): 4 | id = f'todo-{item.id}' 5 | dellink = AX('Delete', hx_delete=f'/todo/{item.id}', target_id=id, hx_swap='delete') 6 | return Li(item.task, dellink, id=id) 7 | 8 | auth = user_pwd_auth(user='s3kret', skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css']) 9 | app,rt,todos,Todo = fast_app( 10 | 'data/tbl.db', middleware=[auth], render=render, 11 | id=int, task=str, pk='id') 12 | 13 | @rt("/") 14 | async def get(request): 15 | new_frm = Form(hx_post='/', target_id='todo-list', hx_swap='beforeend')( 16 | Group( 17 | Input(name='task', placeholder='Task'), 18 | Button('Add') 19 | ) 20 | ) 21 | items = Ul(*todos(), id='todo-list') 22 | logout = A('logout', href=basic_logout(request)) 23 | return Titled('Todo list', new_frm, items, logout) 24 | 25 | @rt("/") 26 | async def post(todo:Todo): return todos.insert(todo) 27 | 28 | @rt("/todo/{id}") 29 | async def delete(id:int): todos.delete(id) 30 | 31 | serve() 32 | -------------------------------------------------------------------------------- /nbs/tutorials/01_quickdraw.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from datetime import datetime 3 | 4 | def render(room): 5 | return Li(A(room.name, href=f"/rooms/{room.id}")) 6 | 7 | app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id') 8 | 9 | @rt("/") 10 | def get(): 11 | # The 'Input' id defaults to the same as the name, so you can omit it if you wish 12 | create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), 13 | Button("Create Room"), 14 | hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") 15 | rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') 16 | return Titled("QuickDraw", create_room, rooms_list) 17 | 18 | @rt("/rooms") 19 | async def post(room:Room): 20 | room.created_at = datetime.now().isoformat() 21 | return rooms.insert(room) 22 | 23 | @rt("/rooms/{id}") 24 | async def get(id:int): 25 | room = rooms[id] 26 | return Titled(f"Room: {room.name}", H1(f"Welcome to {room.name}"), A(Button("Leave Room"), href="/")) 27 | 28 | serve() -------------------------------------------------------------------------------- /nbs/styles.css: -------------------------------------------------------------------------------- 1 | .cell { margin-bottom: 1rem; } 2 | .cell > .sourceCode { margin-bottom: 0; } 3 | .cell-output > pre { margin-bottom: 0; } 4 | 5 | .cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre { 6 | margin-left: 0.8rem; 7 | margin-top: 0; 8 | background: none; 9 | border-left: 2px solid lightsalmon; 10 | border-top-left-radius: 0; 11 | border-top-right-radius: 0; 12 | } 13 | 14 | .cell-output > .sourceCode { border: none; } 15 | 16 | .cell-output > .sourceCode { 17 | background: none; 18 | margin-top: 0; 19 | } 20 | 21 | div.description { 22 | padding-left: 2px; 23 | padding-top: 5px; 24 | font-size: 1.25rem; 25 | color: rgba(0, 0, 0, 0.60); 26 | opacity: 70%; 27 | } 28 | 29 | 30 | div.sidebar-item-container .active { 31 | background-color: #E8E8FC; 32 | color: #000; 33 | } 34 | 35 | div.sidebar-item-container a { 36 | color: #000; 37 | padding: 4px 6px; 38 | border-radius: 6px; 39 | font-size: 1rem; 40 | } 41 | 42 | li.sidebar-item { 43 | margin-top: 3px; 44 | } 45 | 46 | span.menu-text { 47 | line-height: 20px; 48 | } 49 | 50 | .navbar-container { max-width: 1282px; } 51 | 52 | -------------------------------------------------------------------------------- /fasthtml/katex.js: -------------------------------------------------------------------------------- 1 | import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; 2 | import katex from "https://cdn.jsdelivr.net/npm/katex/dist/katex.mjs"; 3 | 4 | const renderMath = (tex, displayMode) => { return katex.renderToString(tex, { 5 | throwOnError: false, displayMode: displayMode, output: 'html', trust: true 6 | }) }; 7 | 8 | const processLatexEnvironments = (content) => { 9 | return content.replace(/\\begin{(\w+)}([\s\S]*?)\\end{\1}/g, (match, env, innerContent) => { 10 | if ([{env_list}].includes(env)) { return `{display_delim}${match}{display_delim}`; } 11 | return match; 12 | }) }; 13 | 14 | proc_htmx('{sel}', e => { 15 | let content = processLatexEnvironments(e.textContent); 16 | // Display math (including environments) 17 | content = content.replace(/{display_delim}([\s\S]+?){display_delim}/gm, (_, tex) => renderMath(tex.trim(), true)); 18 | // Inline math 19 | content = content.replace(/(? renderMath(tex.trim(), false)); 20 | e.innerHTML = marked.parse(content); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | repo = fasthtml 3 | lib_name = python-fasthtml 4 | version = 0.12.37 5 | min_python = 3.10 6 | license = apache2 7 | requirements = fastcore>=1.8.1 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.1.1 python-multipart beautifulsoup4 8 | dev_requirements = ipython lxml pysymbol_llm monsterui PyJWT 9 | black_formatting = False 10 | conda_user = fastai 11 | doc_path = _docs 12 | lib_path = fasthtml 13 | nbs_path = nbs 14 | recursive = True 15 | tst_flags = notest 16 | put_version_in_init = True 17 | branch = main 18 | custom_sidebar = False 19 | doc_host = https://www.fastht.ml 20 | doc_baseurl = /docs/ 21 | git_url = https://github.com/AnswerDotAI/fasthtml 22 | title = fasthtml 23 | audience = Developers 24 | author = Jeremy Howard and contributors 25 | author_email = github@jhoward.fastmail.fm 26 | copyright = 2024 onwards, Jeremy Howard 27 | description = The fastest way to create an HTML app 28 | keywords = nbdev jupyter notebook python 29 | language = English 30 | status = 3 31 | console_scripts = fh_railway_link=fasthtml.cli:railway_link 32 | fh_railway_deploy=fasthtml.cli:railway_deploy 33 | user = AnswerDotAI 34 | readme_nb = index.ipynb 35 | allowed_metadata_keys = 36 | allowed_cell_metadata_keys = 37 | jupyter_hooks = True 38 | clean_ids = True 39 | clear_all = False 40 | cell_number = False 41 | skip_procs = 42 | update_pyproject = True 43 | 44 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | about: Propose changes to the codebase 4 | title: '[PR] ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Related Issue** 11 | Please link to the issue that this pull request addresses. If there isn't one, please create an issue first. 12 | 13 | **Proposed Changes** 14 | Describe the big picture of your changes here. If it fixes a bug or resolves a feature request, be sure to link to that issue. 15 | 16 | **Types of changes** 17 | What types of changes does your code introduce? Put an `x` in all the boxes that apply: 18 | - [ ] Bug fix (non-breaking change which fixes an issue) 19 | - [ ] New feature (non-breaking change which adds functionality) 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 21 | 22 | **Checklist** 23 | Go over all the following points, and put an `x` in all the boxes that apply: 24 | - [ ] My code follows the code style of this project. 25 | - [ ] My change requires a change to the documentation. 26 | - [ ] I have updated the documentation accordingly. 27 | - [ ] I have added tests to cover my changes. 28 | - [ ] All new and existing tests passed. 29 | - [ ] I am aware that this is an nbdev project, and I have edited, cleaned, and synced the source notebooks instead of editing .py or .md files directly. 30 | 31 | **Additional Information** 32 | Any additional information, configuration or data that might be necessary to reproduce the issue. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | If you'd like to discuss your feature idea first with the community (highly recommended!) please visit our [Discord channel](https://discord.gg/qcXvcxMhdP). 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Example code** 19 | Provide an example of how you imagine the feature working: 20 | 21 | ```python 22 | # Your example code here 23 | ``` 24 | 25 | **Similar implementations** 26 | If available, provide links to similar features in other libraries: 27 | 1. [Library Name](link-to-feature) 28 | 2. [Another Library](another-link-to-feature) 29 | 30 | **Problem solved** 31 | Explain what problem this feature would solve and how it would benefit users of the library: 32 | 33 | **Additional context** 34 | Add any other context or screenshots about the feature request here. 35 | 36 | **Confirmation** 37 | Please confirm the following: 38 | - [ ] I have checked the existing issues and pull requests to ensure this feature hasn't been requested before. 39 | - [ ] I have read the project's documentation to ensure this feature doesn't already exist. 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug to help us improve 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Important Notice** 11 | We do not provide support through GitHub issues. For community-based help, please use either: 12 | - GitHub Discussions tab 13 | - Our [Discord channel](https://discord.gg/qcXvcxMhdP) 14 | 15 | If you're reporting a bug, please continue with this template. 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **Minimal Reproducible Example** 21 | Provide a minimal code snippet that reproduces the issue. This is crucial for us to understand and fix the bug quickly. 22 | 23 | ```python 24 | # Your code here 25 | ``` 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Environment Information** 31 | Please provide the following version information: 32 | - fastlite version: 33 | - fastcore version: 34 | - fasthtml version: 35 | 36 | **Confirmation** 37 | Please confirm the following: 38 | - [ ] I have read the FAQ (https://docs.fastht.ml/explains/faq.html) 39 | - [ ] I have provided a minimal reproducible example 40 | - [ ] I have included the versions of fastlite, fastcore, and fasthtml 41 | - [ ] I understand that this is a volunteer open source project with no commercial support. 42 | 43 | **Additional context** 44 | Add any other context about the problem here. 45 | 46 | **Screenshots** 47 | If applicable, add screenshots to help explain your problem. 48 | -------------------------------------------------------------------------------- /nbs/_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | pre-render: 4 | - pysym2md --output_file apilist.txt fasthtml 5 | resources: 6 | - "*.txt" 7 | preview: 8 | navigate: false 9 | port: 3000 10 | 11 | format: 12 | html: 13 | theme: 14 | - cosmo 15 | - custom.scss 16 | css: styles.css 17 | include-after-body: fix-commonmark.html 18 | toc: true 19 | keep-md: true 20 | canonical-url: true 21 | commonmark: default 22 | 23 | website: 24 | favicon: favicon.ico 25 | twitter-card: 26 | creator: "@jeremyphoward" 27 | site: "@answerdotai" 28 | image: https://www.fastht.ml/docs/og-image.png 29 | open-graph: true 30 | repo-actions: [issue] 31 | navbar: 32 | title: false 33 | logo: "logo.svg" 34 | background: primary 35 | search: true 36 | left: 37 | - text: "Home" 38 | href: https://fastht.ml 39 | - text: "Learn" 40 | href: https://fastht.ml/about 41 | right: 42 | - icon: github 43 | href: "https://github.com/answerdotai/fasthtml" 44 | - icon: twitter 45 | href: https://x.com/answerdotai 46 | aria-label: Fast.ai Twitter 47 | sidebar: 48 | style: floating 49 | contents: 50 | - text: "Get Started" 51 | href: index.ipynb 52 | - section: Tutorials 53 | contents: tutorials/* 54 | - section: Explanations 55 | contents: explains/* 56 | - section: Reference 57 | contents: ref/* 58 | - section: Source 59 | contents: api/* 60 | 61 | metadata-files: [nbdev.yml] 62 | -------------------------------------------------------------------------------- /fasthtml/starlette.py: -------------------------------------------------------------------------------- 1 | from starlette.applications import Starlette 2 | from starlette.middleware import Middleware 3 | from starlette.middleware.sessions import SessionMiddleware 4 | from starlette.middleware.cors import CORSMiddleware 5 | from starlette.middleware.authentication import AuthenticationMiddleware 6 | from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser, requires 7 | from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware 8 | from starlette.middleware.trustedhost import TrustedHostMiddleware 9 | from starlette.responses import Response, HTMLResponse, FileResponse, JSONResponse as JSONResponseOrig, RedirectResponse, StreamingResponse 10 | from starlette.requests import Request, HTTPConnection, FormData 11 | from starlette.staticfiles import StaticFiles 12 | from starlette.exceptions import HTTPException 13 | from starlette._utils import is_async_callable 14 | from starlette.convertors import Convertor, StringConvertor, register_url_convertor, CONVERTOR_TYPES 15 | from starlette.routing import Route, Router, Mount, WebSocketRoute 16 | from starlette.exceptions import HTTPException,WebSocketException 17 | from starlette.endpoints import HTTPEndpoint,WebSocketEndpoint 18 | from starlette.config import Config 19 | from starlette.datastructures import CommaSeparatedStrings, Secret, UploadFile, URLPath 20 | from starlette.types import ASGIApp, Receive, Scope, Send 21 | from starlette.concurrency import run_in_threadpool 22 | from starlette.background import BackgroundTask, BackgroundTasks 23 | from starlette.websockets import WebSocketDisconnect, WebSocket 24 | 25 | -------------------------------------------------------------------------------- /examples/todos3.py: -------------------------------------------------------------------------------- 1 | # Run with: python basic_app.py 2 | from fasthtml.common import * 3 | 4 | def render(todo): 5 | show = AX(todo.task, f'/todos/{todo.id}', 'current-todo') 6 | edit = AX('edit', f'/edit/{todo.id}' , 'current-todo') 7 | dt = ' (done)' if todo.done else '' 8 | return Li(show, dt, ' | ', edit, id=f'todo-{todo.id}') 9 | 10 | app,rt,todos,Todo = fast_app('data/todos.db', render, id=int, task=str, done=bool, pk='id') 11 | 12 | @rt("/") 13 | def get(): 14 | inp = Input(id="new-task", name="task", placeholder="New Todo") 15 | add = Form(Group(inp, Button("Add")), hx_post="/", target_id='todo-list', hx_swap="beforeend") 16 | card = Card(Ul(*todos(), id='todo-list'), header=add, footer=Div(id='current-todo')), 17 | return Titled('Todo list', card) 18 | 19 | @rt("/") 20 | def post(todo:Todo): 21 | return todos.insert(todo), Input(id="new-task", name="task", placeholder="New Todo", hx_swap_oob='true') 22 | 23 | @rt("/edit/{id}") 24 | def get(id:int): 25 | res = Form(Group(Input(id="task"), Button("Save")), 26 | Hidden(id="id"), CheckboxX(id="done", label='Done'), 27 | hx_put="/", target_id=f'todo-{id}', id="edit") 28 | return fill_form(res, todos[id]) 29 | 30 | @rt("/") 31 | def put(todo: Todo): return todos.update(todo), clear('current-todo') 32 | 33 | @rt("/todos/{id}") 34 | def get(id:int): 35 | todo = todos[id] 36 | btn = Button('delete', hx_delete=f'/todos/{todo.id}', target_id=f'todo-{id}', hx_swap="outerHTML") 37 | return Div(Div(todo.task), btn) 38 | 39 | @rt("/todos/{id}") 40 | def delete(id:int): 41 | todos.delete(id) 42 | return clear('current-todo') 43 | 44 | serve() 45 | 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Report missing, incomplete, or incorrect documentation 4 | title: "[DOCS]" 5 | labels: documentation 6 | assignees: '' 7 | --- 8 | 9 | **Issue Type** 10 | - [ ] Existing documentation (e.g. typos, errors, outdated information) 11 | - [ ] Suggest new documentation that is currently missing 12 | 13 | **Current Behavior or Documentation Gaps** 14 | For existing documentation: 15 | A clear and concise description of what is incorrect, missing, or confusing. Include a link to the specific documentation page, if applicable. 16 | 17 | For new documentation suggestions: 18 | Describe the functionality or topic that is currently undocumented, and explain briefly why it would be helpful to document it. 19 | 20 | **Suggested Improvement or New Documentation Content** 21 | Provide details on how to improve the documentation or what content should be added. If you're suggesting a new topic, explain what sections, examples, or tutorials would be beneficial. 22 | 23 | **Steps to Reproduce the Problem (if applicable)** 24 | If this is related to a documentation bug (such as incorrect code or instructions), please describe the steps to reproduce and fix the issue. 25 | 26 | **Relevant Links** 27 | If this issue relates to existing features, APIs, or other projects, please provide relevant links to help clarify the issue or suggested documentation. 28 | 29 | **Additional Context** 30 | Provide any other context or screenshots that might help explain the documentation issue. 31 | 32 | **Confirmation** 33 | Please confirm the following: 34 | - [ ] I have checked the existing issues and pull requests to ensure this documentation issue hasn't been reported before. 35 | - [ ] I have read the project's documentation and am confident this issue or suggestion is valid. 36 | -------------------------------------------------------------------------------- /examples/todos1.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | app,rt,todos,Todo = fast_app('data/todos.db', id=int, task=str, done=bool, pk='id') 4 | 5 | def TodoRow(todo): 6 | return Li( 7 | A(todo.task, href=f'/todos/{todo.id}'), 8 | (' (done)' if todo.done else '') + ' | ', 9 | A('edit', href=f'/edit/{todo.id}'), 10 | id=f'todo-{todo.id}' 11 | ) 12 | 13 | def home(): 14 | add = Form( 15 | Group( 16 | Input(name="task", placeholder="New Todo"), 17 | Button("Add") 18 | ), action="/", method='post' 19 | ) 20 | card = Card( 21 | Ul(*map(TodoRow, todos()), id='todo-list'), 22 | header=add, 23 | footer=Div(id='current-todo') 24 | ) 25 | return Titled('Todo list', card) 26 | 27 | @rt("/") 28 | def get(): return home() 29 | 30 | @rt("/") 31 | def post(todo:Todo): 32 | todos.insert(todo) 33 | return home() 34 | 35 | @rt("/update") 36 | def post(todo: Todo): 37 | todos.update(todo) 38 | return home() 39 | 40 | @rt("/remove") 41 | def get(id:int): 42 | todos.delete(id) 43 | return home() 44 | 45 | @rt("/edit/{id}") 46 | def get(id:int): 47 | res = Form( 48 | Group( 49 | Input(id="task"), 50 | Button("Save") 51 | ), 52 | Hidden(id="id"), 53 | CheckboxX(id="done", label='Done'), 54 | A('Back', href='/', role="button"), 55 | action="/update", id="edit", method='post' 56 | ) 57 | frm = fill_form(res, todos[id]) 58 | return Titled('Edit Todo', frm) 59 | 60 | @rt("/todos/{id}") 61 | def get(id:int): 62 | contents = Div( 63 | Div(todos[id].task), 64 | A('Delete', href=f'/remove?id={id}', role="button"), 65 | A('Back', href='/', role="button") 66 | ) 67 | return Titled('Todo details', contents) 68 | 69 | serve() 70 | 71 | -------------------------------------------------------------------------------- /nbs/tutorials/02_quickdraw.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from datetime import datetime 3 | 4 | def render(room): 5 | return Li(A(room.name, href=f"/rooms/{room.id}")) 6 | 7 | app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, pk='id') 8 | 9 | @rt("/") 10 | def get(): 11 | create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), 12 | Button("Create Room"), 13 | hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") 14 | rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') 15 | return Titled("QuickDraw", 16 | create_room, rooms_list) 17 | 18 | @rt("/rooms") 19 | async def post(room:Room): 20 | room.created_at = datetime.now().isoformat() 21 | return rooms.insert(room) 22 | 23 | @rt("/rooms/{id}") 24 | async def get(id:int): 25 | room = rooms[id] 26 | canvas = Canvas(id="canvas", width="800", height="600") 27 | color_picker = Input(type="color", id="color-picker", value="#3CDD8C") 28 | brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") 29 | 30 | js = """ 31 | var canvas = new fabric.Canvas('canvas'); 32 | canvas.isDrawingMode = true; 33 | canvas.freeDrawingBrush.color = '#3CDD8C'; 34 | canvas.freeDrawingBrush.width = 10; 35 | 36 | document.getElementById('color-picker').onchange = function() { 37 | canvas.freeDrawingBrush.color = this.value; 38 | }; 39 | 40 | document.getElementById('brush-size').oninput = function() { 41 | canvas.freeDrawingBrush.width = parseInt(this.value, 10); 42 | }; 43 | """ 44 | 45 | return Titled(f"Room: {room.name}", 46 | A(Button("Leave Room"), href="/"), 47 | canvas, 48 | Div(color_picker, brush_size), 49 | Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"), 50 | Script(js)) 51 | 52 | serve() -------------------------------------------------------------------------------- /tests/test_toaster.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from starlette.testclient import TestClient 3 | 4 | app, rt = fast_app() 5 | setup_toasts(app) 6 | cli = TestClient(app) 7 | 8 | @rt("/set-toast-get") 9 | def get(session): 10 | add_toast(session, "Toast get", "info") 11 | return RedirectResponse('/see-toast') 12 | 13 | @rt("/set-toast-post") 14 | def post(session): 15 | add_toast(session, "Toast post", "info") 16 | return RedirectResponse('/see-toast') 17 | 18 | @rt("/see-toast") 19 | def get(session): 20 | return Titled("Hello, world!", P(str(session))) 21 | 22 | @rt("/see-toast-with-typehint") 23 | def get(session: dict): 24 | return Titled("Hello, world!", P(str(session))) 25 | 26 | @rt("/see-toast-ft-response") 27 | def get(session): 28 | add_toast(session, "Toast FtResponse", "info") 29 | return FtResponse(Titled("Hello, world!", P(str(session)))) 30 | 31 | def test_get_toaster(): 32 | cli.get('/set-toast-get', follow_redirects=False) 33 | res = cli.get('/see-toast') 34 | assert 'Toast get' in res.text 35 | 36 | res = cli.get('/set-toast-get', follow_redirects=True) 37 | assert 'Toast get' in res.text 38 | 39 | def test_post_toaster(): 40 | cli.post('/set-toast-post', follow_redirects=False) 41 | res = cli.get('/see-toast') 42 | assert 'Toast post' in res.text 43 | 44 | def test_ft_response(): 45 | res = cli.get('/see-toast-ft-response') 46 | assert 'Toast FtResponse' in res.text 47 | 48 | def test_get_toaster_with_typehint(): 49 | res = cli.get('/see-toast-with-typehint', follow_redirects=False) 50 | assert 'Toast get' in res.text 51 | 52 | res = cli.get('/see-toast-with-typehint', follow_redirects=True) 53 | assert 'Toast get' in res.text 54 | 55 | def test_toast_container_in_response(): 56 | # toasts will not render correctly if the toast container isn't rendered. 57 | res = cli.get('/see-toast-ft-response') 58 | assert 'id="fh-toast-container"' in res.text 59 | 60 | test_get_toaster() 61 | test_post_toaster() 62 | test_ft_response() 63 | test_toast_container_in_response() 64 | -------------------------------------------------------------------------------- /fasthtml/cli.py: -------------------------------------------------------------------------------- 1 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/09_cli.ipynb. 2 | 3 | # %% auto 0 4 | __all__ = ['railway_link', 'railway_deploy'] 5 | 6 | # %% ../nbs/api/09_cli.ipynb 7 | from fastcore.utils import * 8 | from fastcore.script import call_parse, bool_arg 9 | from subprocess import check_output, run 10 | 11 | import json 12 | 13 | # %% ../nbs/api/09_cli.ipynb 14 | @call_parse 15 | def railway_link(): 16 | "Link the current directory to the current project's Railway service" 17 | j = json.loads(check_output("railway status --json".split())) 18 | prj = j['id'] 19 | idxpath = 'edges', 0, 'node', 'id' 20 | env = nested_idx(j, 'environments', *idxpath) 21 | svc = nested_idx(j, 'services', *idxpath) 22 | 23 | cmd = f"railway link -e {env} -p {prj} -s {svc}" 24 | res = check_output(cmd.split()) 25 | 26 | # %% ../nbs/api/09_cli.ipynb 27 | def _run(a, **kw): 28 | print('#', ' '.join(a)) 29 | run(a) 30 | 31 | # %% ../nbs/api/09_cli.ipynb 32 | @call_parse 33 | def railway_deploy( 34 | name:str, # The project name to deploy 35 | mount:bool_arg=True # Create a mounted volume at /app/data? 36 | ): 37 | """Deploy a FastHTML app to Railway""" 38 | nm,ver = check_output("railway --version".split()).decode().split() 39 | assert nm.startswith('railway'), f'Unexpected railway version string: {nm}' 40 | if ver2tuple(ver)<(3,8): return print("Please update your railway CLI version to 3.8 or higher") 41 | cp = run("railway status --json".split(), capture_output=True) 42 | if not cp.returncode: 43 | print("Checking deployed projects...") 44 | project_name = json.loads(cp.stdout.decode()).get('name') 45 | if project_name == name: return print("This project is already deployed. Run `railway open`.") 46 | reqs = Path('requirements.txt') 47 | if not reqs.exists(): reqs.write_text('python-fasthtml') 48 | _run(f"railway init -n {name}".split()) 49 | _run(f"railway up -c".split()) 50 | _run(f"railway domain".split()) 51 | railway_link.__wrapped__() 52 | if mount: _run(f"railway volume add -m /app/data".split()) 53 | _run(f"railway up -c".split()) 54 | -------------------------------------------------------------------------------- /examples/todos4.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | db = database('data/todos.db') 4 | todos = db.t.todos 5 | if todos not in db.t: todos.create(id=int, task=str, done=bool, pk='id') 6 | Todo = todos.dataclass() 7 | 8 | id_curr = 'current-todo' 9 | def tid(id): return f'todo-{id}' 10 | 11 | css = Style(':root { --pico-font-size: 100%; }') 12 | auth = user_pwd_auth(user='s3kret', skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css']) 13 | app = FastHTML(hdrs=(picolink, css), middleware=[auth]) 14 | rt = app.route 15 | 16 | @rt("/{fname:path}.{ext:static}") 17 | def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}') 18 | 19 | @patch 20 | def __ft__(self:Todo): 21 | show = AX(self.task, f'/todos/{self.id}', id_curr) 22 | edit = AX('edit', f'/edit/{self.id}' , id_curr) 23 | dt = ' (done)' if self.done else '' 24 | return Li(show, dt, ' | ', edit, id=tid(self.id)) 25 | 26 | def mk_input(**kw): return Input(id="new-task", name="task", placeholder="New Todo", **kw) 27 | def clr_details(): return Div(hx_swap_oob='innerHTML', id=id_curr) 28 | 29 | @rt("/") 30 | def get(request): 31 | add = Form(Group(mk_input(), Button("Add")), 32 | hx_post="/", target_id='todo-list', hx_swap="beforeend") 33 | card = Card(Ul(*todos(), id='todo-list'), 34 | header=add, footer=Div(id=id_curr)), 35 | return Titled('Todo list', card) 36 | 37 | @rt("/todos/{id}") 38 | def delete(id:int): 39 | todos.delete(id) 40 | return clr_details() 41 | 42 | @rt("/") 43 | def post(todo:Todo): return todos.insert(todo), mk_input(hx_swap_oob='true') 44 | 45 | @rt("/edit/{id}") 46 | def get(id:int): 47 | res = Form(Group(Input(id="task"), Button("Save")), 48 | Hidden(id="id"), CheckboxX(id="done", label='Done'), 49 | hx_put="/", target_id=tid(id), id="edit") 50 | return fill_form(res, todos[id]) 51 | 52 | @rt("/") 53 | def put(todo: Todo): return todos.update(todo), clr_details() 54 | 55 | @rt("/todos/{id}") 56 | def get(id:int): 57 | todo = todos[id] 58 | btn = Button('delete', hx_delete=f'/todos/{todo.id}', 59 | target_id=tid(todo.id), hx_swap="outerHTML") 60 | return Div(Div(todo.task), btn) 61 | 62 | serve() 63 | 64 | -------------------------------------------------------------------------------- /fasthtml/authmw.py: -------------------------------------------------------------------------------- 1 | import base64, binascii, re 2 | from fasthtml.core import * 3 | from fasthtml.starlette import * 4 | from typing import Mapping 5 | from hmac import compare_digest 6 | 7 | auth_hdrs = {'WWW-Authenticate': 'Basic realm="login"'} 8 | 9 | class BasicAuthMiddleware(MiddlewareBase): 10 | def __init__(self, app, cb, skip=None): self.app,self.cb,self.skip = app,cb,skip or [] 11 | 12 | async def _resp(self, scope, receive, send, resp): 13 | await (send({"type": "websocket.close", "code": 1000}) if scope["type"]=="websocket" else resp(scope, receive, send)) 14 | 15 | async def __call__(self, scope, receive, send) -> None: 16 | conn = await super().__call__(scope, receive, send) 17 | if not conn: return 18 | request = Request(scope, receive) 19 | if not any(re.match(o+'$', request.url.path) for o in self.skip): 20 | res = await self.authenticate(conn) 21 | if not res: res = Response('not authenticated', status_code=401, headers=auth_hdrs) 22 | if isinstance(res, Response): return await self._resp(scope, receive, send, res) 23 | scope["auth"] = res 24 | await self.app(scope, receive, send) 25 | 26 | async def authenticate(self, conn): 27 | if "Authorization" not in conn.headers: return 28 | auth = conn.headers["Authorization"] 29 | try: 30 | scheme, credentials = auth.split() 31 | if scheme.lower() != 'basic': return 32 | decoded = base64.b64decode(credentials).decode("ascii") 33 | except (ValueError, UnicodeDecodeError, binascii.Error) as exc: raise AuthenticationError('Invalid credentials') 34 | user, _, pwd = decoded.partition(":") 35 | if self.cb(user,pwd): return user 36 | 37 | def user_pwd_auth(lookup=None, skip=None, **kwargs): 38 | if isinstance(lookup,Mapping): kwargs = lookup | kwargs 39 | def cb(u,p): 40 | if u=='logout' or not u or not p: return 41 | if callable(lookup): return lookup(u,p) 42 | return compare_digest(kwargs.get(u,'').encode("utf-8"), p.encode("utf-8")) 43 | return Middleware(BasicAuthMiddleware, cb=cb, skip=skip) 44 | 45 | def basic_logout(request): 46 | return f'{request.url.scheme}://logout:logout@{request.headers["host"]}/' 47 | -------------------------------------------------------------------------------- /examples/todos2.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | 3 | id_curr = 'current-todo' 4 | id_list = 'todo-list' 5 | def tid(id): return f'todo-{id}' 6 | 7 | @dataclass 8 | class TodoItem(): 9 | title: str; id: int = -1; done: bool = False 10 | def __ft__(self): 11 | show = AX(self.title, f'/todos/{self.id}', id_curr) 12 | edit = AX('edit', f'/edit/{self.id}' , id_curr) 13 | dt = ' (done)' if self.done else '' 14 | return Li(show, dt, ' | ', edit, id=tid(self.id)) 15 | 16 | TODO_LIST = [TodoItem(id=0, title="Start writing todo list", done=True), 17 | TodoItem(id=1, title="???", done=False), 18 | TodoItem(id=2, title="Profit", done=False)] 19 | 20 | app, rt = fast_app() 21 | 22 | def mk_input(**kw): return Input(id="new-title", name="title", placeholder="New Todo", **kw) 23 | 24 | @app.get("/") 25 | def get_todos(req): 26 | add = Form(hx_post="/", target_id=id_list, hx_swap="beforeend")( 27 | Group(mk_input(), Button("Add")) 28 | ) 29 | card = Card(Ul(*TODO_LIST, id=id_list), 30 | header=add, footer=Div(id=id_curr)), 31 | return Titled('Todo list', card) 32 | 33 | @app.post("/") 34 | def add_item(todo:TodoItem): 35 | todo.id = len(TODO_LIST)+1 36 | TODO_LIST.append(todo) 37 | return todo, mk_input(hx_swap_oob='true') 38 | 39 | def clr_details(): return Div(hx_swap_oob='innerHTML', id=id_curr) 40 | def find_todo(id): return next(o for o in TODO_LIST if o.id==id) 41 | 42 | @app.get("/edit/{id}") 43 | def edit_item(id:int): 44 | todo = find_todo(id) 45 | res = Form(hx_put="/", target_id=tid(id), id="edit")( 46 | Group(Input(id="title"), Button("Save")), 47 | Hidden(id="id"), 48 | CheckboxX(id="done", label='Done'), 49 | ) 50 | return fill_form(res, todo) 51 | 52 | @app.put("/") 53 | def update(todo: TodoItem): 54 | fill_dataclass(todo, find_todo(todo.id)) 55 | return todo, clr_details() 56 | 57 | @app.delete("/todos/{id}") 58 | def del_todo(id:int): 59 | TODO_LIST.remove(find_todo(id)) 60 | return clr_details() 61 | 62 | @app.get("/todos/{id}") 63 | def get_todo(id:int): 64 | todo = find_todo(id) 65 | btn = Button('delete', hx_delete=f'/todos/{todo.id}', 66 | target_id=tid(todo.id), hx_swap="outerHTML") 67 | return Div(Div(todo.title), btn) 68 | 69 | serve() 70 | 71 | -------------------------------------------------------------------------------- /fasthtml/live_reload.py: -------------------------------------------------------------------------------- 1 | from starlette.routing import WebSocketRoute 2 | from fasthtml.basics import FastHTML, Script 3 | 4 | __all__ = ["FastHTMLWithLiveReload"] 5 | 6 | def LiveReloadJs(reload_attempts:int=20, reload_interval:int=1000, **kwargs): 7 | src = """ 8 | (() => { 9 | let attempts = 0; 10 | const connect = () => { 11 | const socket = new WebSocket(`ws://${window.location.host}/live-reload`); 12 | socket.onopen = async() => { 13 | const res = await fetch(window.location.href); 14 | if (res.ok) { 15 | attempts ? window.location.reload() : console.log('LiveReload connected'); 16 | }}; 17 | socket.onclose = () => { 18 | !attempts++ ? connect() : setTimeout(() => { connect() }, %d); 19 | if (attempts > %d) window.location.reload(); 20 | }}; 21 | connect(); 22 | })(); 23 | """ 24 | return Script(src % (reload_attempts, reload_interval)) 25 | 26 | async def live_reload_ws(websocket): await websocket.accept() 27 | 28 | class FastHTMLWithLiveReload(FastHTML): 29 | """ 30 | `FastHTMLWithLiveReload` enables live reloading. 31 | This means that any code changes saved on the server will automatically 32 | trigger a reload of both the server and browser window. 33 | 34 | How does it work? 35 | - a websocket is created at `/live-reload` 36 | - a small js snippet `LIVE_RELOAD_SCRIPT` is injected into each webpage 37 | - this snippet connects to the websocket at `/live-reload` and listens for an `onclose` event 38 | - when the `onclose` event is detected the browser is reloaded 39 | 40 | Why do we listen for an `onclose` event? 41 | When code changes are saved the server automatically reloads if the --reload flag is set. 42 | The server reload kills the websocket connection. The `onclose` event serves as a proxy 43 | for "developer has saved some changes". 44 | 45 | Usage 46 | >>> from fasthtml.common import * 47 | >>> app = FastHTMLWithLiveReload() 48 | 49 | Run: 50 | serve() 51 | """ 52 | def __init__(self, *args, **kwargs): 53 | # "hdrs" and "routes" can be missing, None, a list or a tuple. 54 | kwargs["hdrs"] = [*(kwargs.get("hdrs") or []), LiveReloadJs(**kwargs)] 55 | kwargs["routes"] = [*(kwargs.get("routes") or []), WebSocketRoute("/live-reload", endpoint=live_reload_ws)] 56 | super().__init__(*args, **kwargs) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client_secret.json 2 | # TODO: modify nbdev to not warn for nb_export nbs 3 | fasthtml/stripe_otp.py 4 | posts/ 5 | .quarto 6 | .sesskey 7 | *.db-* 8 | *.db 9 | .gitattributes 10 | _proc/ 11 | sidebar.yml 12 | Gemfile.lock 13 | token 14 | _docs/ 15 | conda/ 16 | .last_checked 17 | .gitconfig 18 | *.bak 19 | *.log 20 | *~ 21 | ~* 22 | _tmp* 23 | tmp* 24 | tags 25 | 26 | # Byte-compiled / optimized / DLL files 27 | __pycache__/ 28 | *.py[cod] 29 | *$py.class 30 | 31 | # C extensions 32 | *.so 33 | 34 | # Distribution / packaging 35 | .Python 36 | env/ 37 | build/ 38 | develop-eggs/ 39 | dist/ 40 | downloads/ 41 | eggs/ 42 | .eggs/ 43 | lib/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | wheels/ 49 | *.egg-info/ 50 | .installed.cfg 51 | *.egg 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .coverage 67 | .coverage.* 68 | .cache 69 | nosetests.xml 70 | coverage.xml 71 | *.cover 72 | .hypothesis/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Django stuff: 79 | *.log 80 | local_settings.py 81 | 82 | # Flask stuff: 83 | instance/ 84 | .webassets-cache 85 | 86 | # Scrapy stuff: 87 | .scrapy 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | # Jupyter Notebook 96 | .ipynb_checkpoints 97 | 98 | # pyenv 99 | .python-version 100 | 101 | # celery beat schedule file 102 | celerybeat-schedule 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # dotenv 108 | .env 109 | 110 | # virtualenv 111 | .venv 112 | venv/ 113 | ENV/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | 128 | .vscode 129 | *.swp 130 | 131 | # osx generated files 132 | .DS_Store 133 | .DS_Store? 134 | .Trashes 135 | ehthumbs.db 136 | Thumbs.db 137 | .idea 138 | 139 | # pytest 140 | .pytest_cache 141 | 142 | # tools/trust-doc-nbs 143 | docs_src/.last_checked 144 | 145 | # symlinks to fastai 146 | docs_src/fastai 147 | tools/fastai 148 | 149 | # link checker 150 | checklink/cookies.txt 151 | 152 | # .gitconfig is now autogenerated 153 | .gitconfig 154 | 155 | _docs 156 | -------------------------------------------------------------------------------- /nbs/ref/live_reload.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Live Reloading" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "\n", 15 | "When building your app it can be useful to view your changes in a web browser as you make them. FastHTML supports live reloading which means that it watches for any changes to your code and automatically refreshes the webpage in your browser.\n", 16 | "\n", 17 | "To enable live reloading simply replace `FastHTML` in your app with `FastHTMLWithLiveReload`.\n", 18 | "\n", 19 | "```python\n", 20 | "from fasthtml.common import *\n", 21 | "app = FastHTMLWithLiveReload()\n", 22 | "```\n", 23 | "\n", 24 | "Then in your terminal run `uvicorn` with reloading enabled.\n", 25 | "\n", 26 | "```\n", 27 | "uvicorn main:app --reload\n", 28 | "```\n", 29 | "\n", 30 | "**⚠️ Gotchas**\n", 31 | "- A reload is only triggered when you save your changes.\n", 32 | "- `FastHTMLWithLiveReload` should only be used during development.\n", 33 | "- If your app spans multiple directories you might need to use the `--reload-dir` flag to watch all files in each directory. See the uvicorn [docs](https://www.uvicorn.org/settings/#development) for more info.\n", 34 | "- The live reload script is only injected into the page when rendering [ft components](https://www.fastht.ml/docs/explains/explaining_xt_components.html)." 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Live reloading with `fast_app`\n", 42 | "\n", 43 | "In development the `fast_app` function provides the same functionality. It instantiates the `FastHTMLWithLiveReload` class if you pass `live=True`:\n", 44 | "\n", 45 | "``` {.python filename=\"main.py\"}\n", 46 | "from fasthtml.common import *\n", 47 | "\n", 48 | "app, rt = fast_app(live=True) # <1>\n", 49 | "\n", 50 | "serve() # <2>\n", 51 | "```\n", 52 | "\n", 53 | "1. `fast_app()` instantiates the `FastHTMLWithLiveReload` class.\n", 54 | "2. `serve()` is a wrapper around a `uvicorn` call.\n", 55 | "\n", 56 | "To run `main.py` in live reload mode, just do `python main.py`. We recommend turning off live reload when deploying your app to production." 57 | ] 58 | } 59 | ], 60 | "metadata": { 61 | "kernelspec": { 62 | "display_name": "python3", 63 | "language": "python", 64 | "name": "python3" 65 | } 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 2 69 | } 70 | -------------------------------------------------------------------------------- /examples/user_app.py: -------------------------------------------------------------------------------- 1 | # Run with: python user_app.py 2 | # At signin, enter a user/pw combination and if it doesn't exist, it will be created. 3 | from fasthtml.common import * 4 | 5 | db = database('data/utodos.db') 6 | class User: name:str; pwd:str 7 | class Todo: id:int; title:str; done:bool; name:str; details:str 8 | users = db.create(User, pk='name') 9 | todos = db.create(Todo) 10 | 11 | id_curr = 'current-todo' 12 | def tid(id): return f'todo-{id}' 13 | 14 | def lookup_user(u,p): 15 | try: user = users[u] 16 | except NotFoundError: user = users.insert(name=u, pwd=p) 17 | return user.pwd==p 18 | 19 | authmw = user_pwd_auth(lookup_user, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css']) 20 | 21 | def before(auth): todos.xtra(name=auth) 22 | 23 | app = FastHTML(middleware=[authmw], before=before, 24 | hdrs=(picolink, 25 | Style(':root { --pico-font-size: 100%; }'))) 26 | rt = app.route 27 | 28 | @rt("/{fname:path}.{ext:static}") 29 | def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}') 30 | 31 | @patch 32 | def __ft__(self:Todo): 33 | show = AX(self.title, f'/todos/{self.id}', id_curr) 34 | edit = AX('edit', f'/edit/{self.id}' , id_curr) 35 | dt = '✅ ' if self.done else '' 36 | return Li(dt, show, ' | ', edit, Hidden(id="id", value=self.id), id=tid(self.id)) 37 | 38 | def mk_input(**kw): return Input(id="new-title", name="title", placeholder="New Todo", **kw) 39 | def clr_details(): return Div(hx_swap_oob='innerHTML', id=id_curr) 40 | 41 | @rt("/") 42 | def get(request, auth): 43 | add = Form(Group(mk_input(), Button("Add")), 44 | hx_post="/", target_id='todo-list', hx_swap="beforeend") 45 | card = Card(Ul(*todos(), id='todo-list'), 46 | header=add, footer=Div(id=id_curr)), 47 | top = Grid(Div(A('logout', href=basic_logout(request)), style='text-align: right')) 48 | return Titled(f"{auth}'s todo list", top, card) 49 | 50 | @rt("/todos/{id}") 51 | def delete(id:int): 52 | todos.delete(id) 53 | return clr_details() 54 | 55 | @rt("/") 56 | def post(todo:Todo): 57 | return todos.insert(todo), mk_input(hx_swap_oob='true') 58 | 59 | @rt("/edit/{id}") 60 | def get(id:int): 61 | res = Form(Group(Input(id="title"), Button("Save")), 62 | Hidden(id="id"), CheckboxX(id="done", label='Done'), 63 | hx_put="/", target_id=tid(id), id="edit") 64 | return fill_form(res, todos[id]) 65 | 66 | @rt("/") 67 | def put(todo: Todo): 68 | return todos.upsert(todo), clr_details() 69 | 70 | @rt("/todos/{id}") 71 | def get(id:int): 72 | todo = todos[id] 73 | btn = Button('delete', hx_delete=f'/todos/{todo.id}', 74 | target_id=tid(todo.id), hx_swap="outerHTML") 75 | return Div(Div(todo.title), btn) 76 | 77 | serve() 78 | 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import parse_version 2 | from configparser import ConfigParser 3 | import setuptools, shlex 4 | assert parse_version(setuptools.__version__)>=parse_version('36.2') 5 | 6 | # note: all settings are in settings.ini; edit there, not here 7 | config = ConfigParser(delimiters=['=']) 8 | config.read('settings.ini', encoding='utf-8') 9 | cfg = config['DEFAULT'] 10 | 11 | cfg_keys = 'version description keywords author author_email'.split() 12 | expected = cfg_keys + "lib_name user branch license status min_python audience language".split() 13 | for o in expected: assert o in cfg, "missing expected setting: {}".format(o) 14 | setup_cfg = {o:cfg[o] for o in cfg_keys} 15 | 16 | licenses = { 17 | 'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'), 18 | 'mit': ('MIT License', 'OSI Approved :: MIT License'), 19 | 'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'), 20 | 'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'), 21 | 'bsd3': ('BSD License', 'OSI Approved :: BSD License'), 22 | } 23 | statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', 24 | '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] 25 | py_versions = '3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13'.split() 26 | 27 | requirements = shlex.split(cfg.get('requirements', '')) 28 | if cfg.get('pip_requirements'): requirements += shlex.split(cfg.get('pip_requirements', '')) 29 | min_python = cfg['min_python'] 30 | lic = licenses.get(cfg['license'].lower(), (cfg['license'], None)) 31 | dev_requirements = (cfg.get('dev_requirements') or '').split() 32 | 33 | setuptools.setup( 34 | name = cfg['lib_name'], 35 | license = lic[0], 36 | classifiers = [ 37 | 'Development Status :: ' + statuses[int(cfg['status'])], 38 | 'Intended Audience :: ' + cfg['audience'].title(), 39 | 'Natural Language :: ' + cfg['language'].title(), 40 | ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []), 41 | url = cfg['git_url'], 42 | packages = setuptools.find_packages(), 43 | include_package_data = True, 44 | install_requires = requirements, 45 | extras_require={ 'dev': dev_requirements }, 46 | dependency_links = cfg.get('dep_links','').split(), 47 | python_requires = '>=' + cfg['min_python'], 48 | long_description = open('README.md', encoding='utf-8').read(), 49 | long_description_content_type = 'text/markdown', 50 | zip_safe = False, 51 | entry_points = { 52 | 'console_scripts': cfg.get('console_scripts','').split(), 53 | 'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d'] 54 | }, 55 | **setup_cfg) 56 | 57 | 58 | -------------------------------------------------------------------------------- /nbs/tutorials/03_quickdraw.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from datetime import datetime 3 | 4 | def render(room): 5 | return Li(A(room.name, href=f"/rooms/{room.id}")) 6 | 7 | app,rt,rooms,Room = fast_app('data/drawapp.db', render=render, id=int, name=str, created_at=str, canvas_data=str, pk='id') 8 | 9 | @rt("/") 10 | def get(): 11 | create_room = Form(Input(id="name", name="name", placeholder="New Room Name"), 12 | Button("Create Room"), 13 | hx_post="/rooms", hx_target="#rooms-list", hx_swap="afterbegin") 14 | rooms_list = Ul(*rooms(order_by='id DESC'), id='rooms-list') 15 | return Titled("QuickDraw", 16 | create_room, rooms_list) 17 | 18 | @rt("/rooms") 19 | async def post(room:Room): 20 | room.created_at = datetime.now().isoformat() 21 | return rooms.insert(room) 22 | 23 | @rt("/rooms/{id}") 24 | async def get(id:int): 25 | room = rooms[id] 26 | canvas = Canvas(id="canvas", width="800", height="600") 27 | color_picker = Input(type="color", id="color-picker", value="#3CDD8C") 28 | brush_size = Input(type="range", id="brush-size", min="1", max="50", value="10") 29 | save_button = Button("Save Canvas", id="save-canvas", hx_post=f"/rooms/{id}/save", hx_vals="js:{canvas_data: JSON.stringify(canvas.toJSON())}") 30 | 31 | js = f""" 32 | var canvas = new fabric.Canvas('canvas'); 33 | canvas.isDrawingMode = true; 34 | canvas.freeDrawingBrush.color = '#3CDD8C'; 35 | canvas.freeDrawingBrush.width = 10; 36 | 37 | // Load existing canvas data 38 | fetch(`/rooms/{id}/load`) 39 | .then(response => response.json()) 40 | .then(data => {{ 41 | if (data && Object.keys(data).length > 0) {{ 42 | canvas.loadFromJSON(data, canvas.renderAll.bind(canvas)); 43 | }} 44 | }}); 45 | 46 | document.getElementById('color-picker').onchange = function() {{ 47 | canvas.freeDrawingBrush.color = this.value; 48 | }}; 49 | 50 | document.getElementById('brush-size').oninput = function() {{ 51 | canvas.freeDrawingBrush.width = parseInt(this.value, 10); 52 | }}; 53 | """ 54 | 55 | return Titled(f"Room: {room.name}", 56 | A(Button("Leave Room"), href="/"), 57 | canvas, 58 | Div(color_picker, brush_size, save_button), 59 | Script(src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"), 60 | Script(js)) 61 | 62 | @rt("/rooms/{id}/save") 63 | async def post(id:int, canvas_data:str): 64 | rooms.update({'canvas_data': canvas_data}, id) 65 | return "Canvas saved successfully" 66 | 67 | @rt("/rooms/{id}/load") 68 | async def get(id:int): 69 | room = rooms[id] 70 | return room.canvas_data if room.canvas_data else "{}" 71 | 72 | serve() -------------------------------------------------------------------------------- /fasthtml/toaster.py: -------------------------------------------------------------------------------- 1 | from fastcore.xml import FT 2 | from fasthtml.core import FtResponse 3 | from fasthtml.components import * 4 | from fasthtml.xtend import * 5 | 6 | tcid='fh-toast-container' 7 | sk = "toasts" 8 | toast_css = """ 9 | #fh-toast-container { 10 | position: fixed; inset: 20px 0; z-index: 1090; max-width: 80vw; 11 | display: flex; flex-direction: column; align-items: center; 12 | gap: 10px; pointer-events: none; margin: 0 auto; 13 | } 14 | .fh-toast { 15 | background-color: #333; color: white; 16 | padding: 12px 28px 12px 20px; border-radius: 4px; 17 | text-align: center; opacity: 0.9; position: relative; 18 | & > * { pointer-events: auto; }; 19 | transition: opacity 150ms ease-in-out; min-width: 400px; 20 | @starting-style { opacity: 0.2; }; 21 | } 22 | .fh-toast-dismiss { 23 | position: absolute; top: .2em; right: .4em; 24 | line-height: 1rem; padding: 0 .2em .2em .2em; 25 | border-radius: 15%; filter:drop-shadow(0 0 1px black); 26 | background: inherit; color:inherit; 27 | transition: filter 150ms ease-in-out; 28 | filter:brightness(0.8); &:hover { filter:brightness(0.9); } 29 | } 30 | .fh-toast-info { background-color: #2196F3; } 31 | .fh-toast-success { background-color: #4CAF50; } 32 | .fh-toast-warning { background-color: #FF9800; } 33 | .fh-toast-error { background-color: #F44336; } 34 | """ 35 | 36 | js = '''htmx.onLoad(() => { 37 | if (!htmx.find('#fh-toast-container')) { 38 | const ctn = document.createElement('div'); 39 | ctn.id = 'fh-toast-container'; 40 | document.body.appendChild(ctn); 41 | } 42 | });''' 43 | 44 | def Toast(message: str, typ: str = "info", dismiss: bool = False, duration:int=5000): 45 | x_btn = Button('x', cls="fh-toast-dismiss", onclick="htmx.remove(this?.parentElement);") if dismiss else None 46 | return Div(Span(message), x_btn, cls=f"fh-toast fh-toast-{typ}", hx_on_transitionend=f"setTimeout(() => this?.remove(), {duration});") 47 | 48 | def add_toast(sess, message: str, typ: str = "info", dismiss: bool = False): 49 | assert typ in ("info", "success", "warning", "error"), '`typ` not in ("info", "success", "warning", "error")' 50 | sess.setdefault(sk, []).append((message, typ, dismiss)) 51 | 52 | def render_toasts(sess): 53 | toasts = [Toast(msg, typ, dismiss, sess['toast_duration']) for msg, typ, dismiss in sess.pop(sk, [])] 54 | return Div(*toasts, id=tcid, hx_swap_oob=f'beforeend:#{tcid}') 55 | 56 | def toast_after(resp, req, sess): 57 | if sk in sess and (not resp or isinstance(resp, (tuple,FT,FtResponse))): 58 | sess['toast_duration'] = req.app.state.toast_duration 59 | req.injects.append(render_toasts(sess)) 60 | 61 | def setup_toasts(app, duration=5000): 62 | app.state.toast_duration = duration 63 | app.hdrs += [Style(toast_css), Script(js)] 64 | app.after.append(toast_after) 65 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | Examples of unacceptable behavior by participants include: 13 | 14 | * The use of sexualized language or imagery and unwelcome sexual attention or 15 | advances 16 | * Trolling, insulting/derogatory comments, and personal or political attacks 17 | * Public or private harassment 18 | * Publishing others' private information, such as a physical or electronic 19 | address, without explicit permission 20 | 21 | These examples of unacceptable behaviour are requirements; we will not allow them 22 | in any fast.ai project, including this one. 23 | 24 | ## Our Standards 25 | 26 | Examples of behavior that contributes to creating a positive environment 27 | include: 28 | 29 | * Using welcoming and inclusive language 30 | * Being respectful of differing viewpoints and experiences 31 | * Gracefully accepting constructive criticism 32 | * Focusing on what is best for the community 33 | * Showing empathy towards other community members 34 | 35 | These examples are shown only to help you participate effectively -- they are not 36 | requirements, just requests and guidance. 37 | 38 | ## Our Responsibilities 39 | 40 | Project maintainers are responsible for clarifying the standards of acceptable 41 | behavior and are expected to take appropriate and fair corrective action in 42 | response to any instances of unacceptable behavior. 43 | 44 | Project maintainers have the right and responsibility to remove, edit, or 45 | reject comments, commits, code, wiki edits, issues, and other contributions 46 | that are not aligned to this Code of Conduct, or to ban temporarily or 47 | permanently any contributor for other behaviors that they deem inappropriate, 48 | threatening, offensive, or harmful. 49 | 50 | ## Scope 51 | 52 | This Code of Conduct applies both within project spaces and in public spaces 53 | when an individual is representing the project or its community. Examples of 54 | representing a project or community include using an official project e-mail 55 | address, posting via an official social media account or acting as an appointed 56 | representative at an online or offline event. Representation of a project may be 57 | further defined and clarified by project maintainers. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing or otherwise unacceptable behavior may be 62 | reported by contacting the project team at info@fast.ai. All 63 | complaints will be reviewed and investigated and will result in a response that 64 | is deemed necessary and appropriate to the circumstances. The project team is 65 | obligated to maintain confidentiality with regard to the reporter of an incident. 66 | Further details of specific enforcement policies may be posted separately. 67 | 68 | Project maintainers who do not follow or enforce the Code of Conduct in good 69 | faith may face temporary or permanent repercussions as determined by other 70 | members of the project's leadership. 71 | 72 | ## Attribution 73 | 74 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 75 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | -------------------------------------------------------------------------------- /fasthtml/pico.py: -------------------------------------------------------------------------------- 1 | """Basic components for generating Pico CSS tags""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/04_pico.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['picocss', 'picolink', 'picocondcss', 'picocondlink', 'set_pico_cls', 'Card', 'Group', 'Search', 'Grid', 'DialogX', 7 | 'Container', 'PicoBusy'] 8 | 9 | # %% ../nbs/api/04_pico.ipynb 10 | from typing import Any 11 | 12 | from fastcore.utils import * 13 | from fastcore.xml import * 14 | from fastcore.meta import use_kwargs, delegates 15 | from .components import * 16 | from .xtend import * 17 | 18 | try: from IPython import display 19 | except ImportError: display=None 20 | 21 | # %% ../nbs/api/04_pico.ipynb 22 | picocss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css" 23 | picolink = (Link(rel="stylesheet", href=picocss), 24 | Style(":root { --pico-font-size: 100%; }")) 25 | picocondcss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.conditional.min.css" 26 | picocondlink = (Link(rel="stylesheet", href=picocondcss), 27 | Style(":root { --pico-font-size: 100%; }")) 28 | 29 | # %% ../nbs/api/04_pico.ipynb 30 | def set_pico_cls(): 31 | js = """var sel = '.cell-output, .output_area'; 32 | document.querySelectorAll(sel).forEach(e => e.classList.add('pico')); 33 | 34 | new MutationObserver(ms => { 35 | ms.forEach(m => { 36 | m.addedNodes.forEach(n => { 37 | if (n.nodeType === 1) { 38 | var nc = n.classList; 39 | if (nc && (nc.contains('cell-output') || nc.contains('output_area'))) nc.add('pico'); 40 | n.querySelectorAll(sel).forEach(e => e.classList.add('pico')); 41 | } 42 | }); 43 | }); 44 | }).observe(document.body, { childList: true, subtree: true });""" 45 | return display.Javascript(js) 46 | 47 | # %% ../nbs/api/04_pico.ipynb 48 | @delegates(ft_hx, keep=True) 49 | def Card(*c, header=None, footer=None, **kwargs)->FT: 50 | "A PicoCSS Card, implemented as an Article with optional Header and Footer" 51 | if header: c = (Header(header),) + c 52 | if footer: c += (Footer(footer),) 53 | return Article(*c, **kwargs) 54 | 55 | # %% ../nbs/api/04_pico.ipynb 56 | @delegates(ft_hx, keep=True) 57 | def Group(*c, **kwargs)->FT: 58 | "A PicoCSS Group, implemented as a Fieldset with role 'group'" 59 | return Fieldset(*c, role="group", **kwargs) 60 | 61 | # %% ../nbs/api/04_pico.ipynb 62 | @delegates(ft_hx, keep=True) 63 | def Search(*c, **kwargs)->FT: 64 | "A PicoCSS Search, implemented as a Form with role 'search'" 65 | return Form(*c, role="search", **kwargs) 66 | 67 | # %% ../nbs/api/04_pico.ipynb 68 | @delegates(ft_hx, keep=True) 69 | def Grid(*c, cls='grid', **kwargs)->FT: 70 | "A PicoCSS Grid, implemented as child Divs in a Div with class 'grid'" 71 | c = tuple(o if isinstance(o,list) else Div(o) for o in c) 72 | return ft_hx('div', *c, cls=cls, **kwargs) 73 | 74 | # %% ../nbs/api/04_pico.ipynb 75 | @delegates(ft_hx, keep=True) 76 | def DialogX(*c, open=None, header=None, footer=None, id=None, **kwargs)->FT: 77 | "A PicoCSS Dialog, with children inside a Card" 78 | card = Card(*c, header=header, footer=footer, **kwargs) 79 | return Dialog(card, open=open, id=id) 80 | 81 | # %% ../nbs/api/04_pico.ipynb 82 | @delegates(ft_hx, keep=True) 83 | def Container(*args, **kwargs)->FT: 84 | "A PicoCSS Container, implemented as a Main with class 'container'" 85 | return Main(*args, cls="container", **kwargs) 86 | 87 | # %% ../nbs/api/04_pico.ipynb 88 | def PicoBusy(): 89 | return (HtmxOn('beforeRequest', "event.detail.elt.setAttribute('aria-busy', 'true' )"), 90 | HtmxOn('afterRequest', "event.detail.elt.setAttribute('aria-busy', 'false')")) 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Make sure you have read the [doc on code style]( 4 | https://docs.fast.ai/dev/style.html) first. (Note that we don't follow PEP8, but instead follow a coding style designed specifically for numerical and interactive programming.) 5 | 6 | This project uses [nbdev](https://nbdev.fast.ai/getting_started.html) for development. Before beginning, make sure that nbdev and a jupyter-compatible client such as jupyterlab or nbclassic are installed. To make changes to the codebase, update the notebooks in the `nbs` folder, not the .py files directly. Then, run `nbdev_export`. For more details, have a look at the [nbdev tutorial](https://nbdev.fast.ai/tutorials/tutorial.html). Depending on the code changes, you might also need to run `tools/update.sh` to update python interface modules and [LLM reference material](https://github.com/AnswerDotAI/llms-txt). 7 | 8 | You may want to set up a `prep` alias in `~/.zshrc` or other shell startup file: 9 | 10 | ```sh 11 | alias prep='nbdev_export && nbdev_clean && nbdev_trust' 12 | ``` 13 | 14 | Run `prep` before each commit to ensure your python files are up to date, and you notebooks cleaned of metadata and notarized. 15 | 16 | ## Updating README.md 17 | 18 | Similar to updating Python source code files, to update the `README.md` file you will need to edit a notebook file, specifically `nbs/index.ipynb`. 19 | 20 | However, there are a couple of extra dependencies that you need to install first in order to make this work properly. Go to the directory you cloned the repo to, and type: 21 | 22 | ``` 23 | pip install -e '.[dev]' 24 | ``` 25 | 26 | And install quarto too: 27 | 28 | ``` 29 | nbdev_install_quarto 30 | ``` 31 | 32 | Then, after you make subsequent changes to `nbs/index.ipynb`, run the following from the repo's root directory to (re)build `README.md`: 33 | 34 | ``` 35 | nbdev_readme 36 | ``` 37 | 38 | ## Did you find a bug? 39 | 40 | * Ensure the bug was not already reported by searching on GitHub under Issues. 41 | * If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 42 | * Be sure to add the complete error messages. 43 | 44 | ### Did you write a patch that fixes a bug? 45 | 46 | * Open a new GitHub pull request with the patch. 47 | * Ensure that your PR includes a test that fails without your patch, and pass with it. 48 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 49 | 50 | ## PR submission guidelines 51 | 52 | * Keep each PR focused. While it's more convenient, do not combine several unrelated fixes together. Create as many branches as needed to keep each PR focused. 53 | * Do not mix style changes/fixes with "functional" changes. It's very difficult to review such PRs and will most likely get rejected. 54 | * Do not add/remove vertical whitespace. Preserve the original style of the file you edit as much as you can. 55 | * Do not turn an already-submitted PR into your development playground. If after you submit a PR, you discover that more work is needed: close the PR, do the required work, and then submit a new PR. Otherwise each of your commits requires attention from maintainers of the project. 56 | * If, however, you submit a PR and receive a request for changes, you should proceed with commits inside that PR, so that the maintainer can see the incremental fixes and won't need to review the whole PR again. In the exception case where you realize it'll take many many commits to complete the requests, then it's probably best to close the PR, do the work, and then submit it again. Use common sense where you'd choose one way over another. 57 | 58 | ## Do you want to contribute to the documentation? 59 | 60 | * Docs are automatically created from the notebooks in the nbs folder. 61 | -------------------------------------------------------------------------------- /examples/adv_app_strip.py: -------------------------------------------------------------------------------- 1 | from fasthtml.common import * 2 | from hmac import compare_digest 3 | 4 | db = database('data/utodos.db') 5 | class User: name:str; pwd:str 6 | class Todo: id:int; title:str; done:bool; name:str; details:str; priority:int 7 | users = db.create(User, pk='name') 8 | todos = db.create(Todo, transform=True) 9 | 10 | login_redir = RedirectResponse('/login', status_code=303) 11 | 12 | def before(req, sess): 13 | auth = req.scope['auth'] = sess.get('auth', None) 14 | if not auth: return login_redir 15 | todos.xtra(name=auth) 16 | 17 | bware = Beforeware(before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', '/login', '/send_login']) 18 | 19 | def _not_found(req, exc): return Titled('Oh no!', Div('We could not find that page :(')) 20 | app,rt = fast_app(before=bware, exception_handlers={404: _not_found}, 21 | hdrs=(SortableJS('.sortable'), MarkdownJS())) 22 | 23 | @rt 24 | def login(): 25 | frm = Form(action=send_login, method='post')( 26 | Input(id='name', placeholder='Name'), 27 | Input(id='pwd', type='password', placeholder='Password'), 28 | Button('login')) 29 | return Titled("Login", frm) 30 | 31 | @rt 32 | def send_login(name:str, pwd:str, sess): 33 | if not name or not pwd: return login_redir 34 | try: u = users[name] 35 | except NotFoundError: u = users.insert(name=name, pwd=pwd) 36 | if not compare_digest(u.pwd.encode("utf-8"), pwd.encode("utf-8")): return login_redir 37 | sess['auth'] = u.name 38 | return RedirectResponse('/', status_code=303) 39 | 40 | @rt 41 | def logout(sess): 42 | del sess['auth'] 43 | return login_redir 44 | 45 | def clr_details(): return Div(hx_swap_oob='innerHTML', id='current-todo') 46 | 47 | @rt 48 | def update(todo: Todo): return todos.update(todo), clr_details() 49 | 50 | @rt 51 | def edit(id:int): 52 | res = Form(hx_post=update, target_id=f'todo-{id}', id="edit")( 53 | Group(Input(id="title"), Button("Save")), 54 | Hidden(id="id"), CheckboxX(id="done", label='Done'), 55 | Textarea(id="details", name="details", rows=10)) 56 | return fill_form(res, todos[id]) 57 | 58 | @rt 59 | def rm(id:int): 60 | todos.delete(id) 61 | return clr_details() 62 | 63 | @rt 64 | def show(id:int): 65 | todo = todos[id] 66 | btn = Button('delete', hx_post=rm.to(id=todo.id), 67 | hx_target=f'#todo-{todo.id}', hx_swap="outerHTML") 68 | return Div(H2(todo.title), Div(todo.details, cls="marked"), btn) 69 | 70 | @patch 71 | def __ft__(self:Todo): 72 | ashow = AX(self.title, show.to(id=self.id), 'current-todo') 73 | aedit = AX('edit', edit.to(id=self.id), 'current-todo') 74 | dt = '✅ ' if self.done else '' 75 | cts = (dt, ashow, ' | ', aedit, Hidden(id="id", value=self.id), Hidden(id="priority", value="0")) 76 | return Li(*cts, id=f'todo-{self.id}') 77 | 78 | @rt 79 | def create(todo:Todo): 80 | new_inp = Input(id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') 81 | return todos.insert(todo), new_inp 82 | 83 | @rt 84 | def reorder(id:list[int]): 85 | for i,id_ in enumerate(id): todos.update({'priority':i}, id_) 86 | return tuple(todos(order_by='priority')) 87 | 88 | @rt 89 | def index(auth): 90 | title = f"{auth}'s Todo list" 91 | top = Grid(H1(title), Div(A('logout', href=logout), style='text-align: right')) 92 | new_inp = Input(id="new-title", name="title", placeholder="New Todo") 93 | add = Form(Group(new_inp, Button("Add")), 94 | hx_post=create, target_id='todo-list', hx_swap="afterbegin") 95 | frm = Form(*todos(order_by='priority'), 96 | id='todo-list', cls='sortable', hx_post=reorder, hx_trigger="end") 97 | card = Card(P('Drag/drop todos to reorder them'), 98 | Ul(frm), 99 | header=add, footer=Div(id='current-todo')) 100 | return Title(title), Container(top, card) 101 | 102 | serve() 103 | 104 | -------------------------------------------------------------------------------- /nbs/api/09_cli.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "b73068da", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "#| default_exp cli" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "27a7e6e2", 16 | "metadata": {}, 17 | "source": [ 18 | "# Command Line Tools" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "acfbc502", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "#| export\n", 29 | "from fastcore.utils import *\n", 30 | "from fastcore.script import call_parse, bool_arg\n", 31 | "from subprocess import check_output, run\n", 32 | "\n", 33 | "import json" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "11d71cfc", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "#| export\n", 44 | "@call_parse\n", 45 | "def railway_link():\n", 46 | " \"Link the current directory to the current project's Railway service\"\n", 47 | " j = json.loads(check_output(\"railway status --json\".split()))\n", 48 | " prj = j['id']\n", 49 | " idxpath = 'edges', 0, 'node', 'id'\n", 50 | " env = nested_idx(j, 'environments', *idxpath)\n", 51 | " svc = nested_idx(j, 'services', *idxpath)\n", 52 | "\n", 53 | " cmd = f\"railway link -e {env} -p {prj} -s {svc}\"\n", 54 | " res = check_output(cmd.split())" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "586830f6", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "#| export\n", 65 | "def _run(a, **kw):\n", 66 | " print('#', ' '.join(a))\n", 67 | " run(a)" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "id": "b84fd5ff", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "#| export\n", 78 | "@call_parse\n", 79 | "def railway_deploy(\n", 80 | " name:str, # The project name to deploy\n", 81 | " mount:bool_arg=True # Create a mounted volume at /app/data?\n", 82 | "):\n", 83 | " \"\"\"Deploy a FastHTML app to Railway\"\"\"\n", 84 | " nm,ver = check_output(\"railway --version\".split()).decode().split()\n", 85 | " assert nm.startswith('railway'), f'Unexpected railway version string: {nm}'\n", 86 | " if ver2tuple(ver)<(3,8): return print(\"Please update your railway CLI version to 3.8 or higher\")\n", 87 | " cp = run(\"railway status --json\".split(), capture_output=True)\n", 88 | " if not cp.returncode:\n", 89 | " print(\"Checking deployed projects...\")\n", 90 | " project_name = json.loads(cp.stdout.decode()).get('name')\n", 91 | " if project_name == name: return print(\"This project is already deployed. Run `railway open`.\")\n", 92 | " reqs = Path('requirements.txt')\n", 93 | " if not reqs.exists(): reqs.write_text('python-fasthtml')\n", 94 | " _run(f\"railway init -n {name}\".split())\n", 95 | " _run(f\"railway up -c\".split())\n", 96 | " _run(f\"railway domain\".split())\n", 97 | " railway_link.__wrapped__()\n", 98 | " if mount: _run(f\"railway volume add -m /app/data\".split())\n", 99 | " _run(f\"railway up -c\".split())" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "id": "474e14b4", 105 | "metadata": {}, 106 | "source": [ 107 | "## Export -" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "id": "d211e8e2", 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "#| hide\n", 118 | "import nbdev; nbdev.nbdev_export()" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "id": "7b1d43a0", 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [] 128 | } 129 | ], 130 | "metadata": { 131 | "kernelspec": { 132 | "display_name": "python3", 133 | "language": "python", 134 | "name": "python3" 135 | } 136 | }, 137 | "nbformat": 4, 138 | "nbformat_minor": 5 139 | } 140 | -------------------------------------------------------------------------------- /nbs/llms.txt: -------------------------------------------------------------------------------- 1 | # FastHTML 2 | 3 | > FastHTML is a python library which brings together Starlette, Uvicorn, HTMX, and fastcore's `FT` "FastTags" into a library for creating server-rendered hypermedia applications. The `FastHTML` class itself inherits from `Starlette`, and adds decorator-based routing with many additions, Beforeware, automatic `FT` to HTML rendering, and much more. 4 | 5 | Things to remember when writing FastHTML apps: 6 | 7 | - Although parts of its API are inspired by FastAPI, it is *not* compatible with FastAPI syntax and is not targeted at creating API services 8 | - FastHTML includes support for Pico CSS and the fastlite sqlite library, although using both are optional; sqlalchemy can be used directly or via the fastsql library, and any CSS framework can be used. Support for the Surreal and css-scope-inline libraries are also included, but both are optional 9 | - FastHTML is compatible with JS-native web components and any vanilla JS library, but not with React, Vue, or Svelte 10 | - Use `serve()` for running uvicorn (`if __name__ == "__main__"` is not needed since it's automatic) 11 | - When a title is needed with a response, use `Titled`; note that that already wraps children in `Container`, and already includes both the meta title as well as the H1 element. 12 | 13 | ## Docs 14 | 15 | - [FastHTML concise guide](https://www.fastht.ml/docs/ref/concise_guide.html.md): A brief overview of idiomatic FastHTML apps 16 | - [HTMX reference](https://raw.githubusercontent.com/bigskysoftware/htmx/master/www/content/reference.md): Brief description of all HTMX attributes, CSS classes, headers, events, extensions, js lib methods, and config options 17 | - [Starlette quick guide](https://gist.githubusercontent.com/jph00/e91192e9bdc1640f5421ce3c904f2efb/raw/61a2774912414029edaf1a55b506f0e283b93c46/starlette-quick.md): A quick overview of some Starlette features useful to FastHTML devs. 18 | 19 | ## API 20 | 21 | - [API List](https://www.fastht.ml/docs/apilist.txt): A succint list of all functions and methods in fasthtml. 22 | - [MonsterUI API List](https://raw.githubusercontent.com/AnswerDotAI/MonsterUI/refs/heads/main/docs/apilist.txt): Complete API Reference for Monster UI, a component framework similar to shadcn, but for FastHTML 23 | 24 | 25 | ## Examples 26 | 27 | - [Websockets application](https://raw.githubusercontent.com/AnswerDotAI/fasthtml/main/examples/basic_ws.py): Very brief example of using websockets with HTMX and FastHTML 28 | - [Todo list application](https://raw.githubusercontent.com/AnswerDotAI/fasthtml/main/examples/adv_app.py): Detailed walk-thru of a complete CRUD app in FastHTML showing idiomatic use of FastHTML and HTMX patterns. 29 | 30 | ## Optional 31 | 32 | - [Surreal](https://raw.githubusercontent.com/AnswerDotAI/surreal/main/README.md): Tiny jQuery alternative for plain Javascript with inline Locality of Behavior, providing `me` and `any` functions 33 | - [Starlette full documentation](https://gist.githubusercontent.com/jph00/809e4a4808d4510be0e3dc9565e9cbd3/raw/9b717589ca44cedc8aaf00b2b8cacef922964c0f/starlette-sml.md): A subset of the Starlette documentation useful for FastHTML development. 34 | - [JS App Walkthrough](https://www.fastht.ml/docs/tutorials/e2e.html.md): An end-to-end walkthrough of a complete FastHTML app, including deployment to railway. 35 | - [FastHTML by Example](https://www.fastht.ml/docs/tutorials/by_example.html.md): A collection of 4 FastHTML apps showcasing idiomatic use of FastHTML and HTMX patterns. 36 | - [Using Jupyter to write FastHTML](https://www.fastht.ml/docs/tutorials/jupyter_and_fasthtml.html.md): A guide to developing FastHTML apps inside Jupyter notebooks. 37 | - [FT Components](https://www.fastht.ml/docs/explains/explaining_xt_components.html.md): Explanation of the `FT` components, which are a way to write HTML in a Pythonic way. 38 | - [FAQ](https://www.fastht.ml/docs/explains/faq.html.md): Answers to common questions about FastHTML. 39 | - [MiniDataAPI Spec](https://www.fastht.ml/docs/explains/minidataapi.html.md): Explanation of the MiniDataAPI specification, which allows us to use the same API for many different database engines. 40 | - [OAuth](https://www.fastht.ml/docs/explains/oauth.html.md): Tutorial and explanation of how to use OAuth in FastHTML apps. 41 | - [Routes](https://www.fastht.ml/docs/explains/routes.html.md): Explanation of how routes work in FastHTML. 42 | - [WebSockets](https://www.fastht.ml/docs/explains/websockets.html.md): Explanation of websockets and how they work in FastHTML. 43 | - [Custom Components](https://www.fastht.ml/docs/ref/defining_xt_component.md): Explanation of how to create custom components in FastHTML. 44 | - [Handling Handlers](https://www.fastht.ml/docs/ref/handlers.html.md): Explanation of how to request and response handlers work in FastHTML as routes. 45 | - [Live Reloading](https://www.fastht.ml/docs/ref/live_reload.html.md): Explanation of how to use live reloading for FastHTML development. 46 | -------------------------------------------------------------------------------- /fasthtml/fastapp.py: -------------------------------------------------------------------------------- 1 | """The `fast_app` convenience wrapper""" 2 | 3 | import inspect,uvicorn 4 | from fastcore.utils import * 5 | from fastlite import * 6 | from .basics import * 7 | from .pico import * 8 | from .starlette import * 9 | from .live_reload import FastHTMLWithLiveReload 10 | 11 | __all__ = ['fast_app'] 12 | 13 | def _get_tbl(dt, nm, schema): 14 | render = schema.pop('render', None) 15 | tbl = dt[nm] 16 | if tbl not in dt: tbl.create(**schema) 17 | else: tbl.create(**schema, transform=True) 18 | dc = tbl.dataclass() 19 | if render: dc.__ft__ = render 20 | return tbl,dc 21 | 22 | def _app_factory(*args, **kwargs) -> FastHTML | FastHTMLWithLiveReload: 23 | "Creates a FastHTML or FastHTMLWithLiveReload app instance" 24 | if kwargs.pop('live', False): return FastHTMLWithLiveReload(*args, **kwargs) 25 | kwargs.pop('reload_attempts', None) 26 | kwargs.pop('reload_interval', None) 27 | return FastHTML(*args, **kwargs) 28 | 29 | def fast_app( 30 | db_file:Optional[str]=None, # Database file name, if needed 31 | render:Optional[callable]=None, # Function used to render default database class 32 | hdrs:Optional[tuple]=None, # Additional FT elements to add to 33 | ftrs:Optional[tuple]=None, # Additional FT elements to add to end of 34 | tbls:Optional[dict]=None, # Experimental mapping from DB table names to dict table definitions 35 | before:Optional[tuple]|Beforeware=None, # Functions to call prior to calling handler 36 | middleware:Optional[tuple]=None, # Standard Starlette middleware 37 | live:bool=False, # Enable live reloading 38 | debug:bool=False, # Passed to Starlette, indicating if debug tracebacks should be returned on errors 39 | title:str="FastHTML page", # Default page title 40 | routes:Optional[tuple]=None, # Passed to Starlette 41 | exception_handlers:Optional[dict]=None, # Passed to Starlette 42 | on_startup:Optional[callable]=None, # Passed to Starlette 43 | on_shutdown:Optional[callable]=None, # Passed to Starlette 44 | lifespan:Optional[callable]=None, # Passed to Starlette 45 | default_hdrs=True, # Include default FastHTML headers such as HTMX script? 46 | pico:Optional[bool]=None, # Include PicoCSS header? 47 | surreal:Optional[bool]=True, # Include surreal.js/scope headers? 48 | htmx:Optional[bool]=True, # Include HTMX header? 49 | exts:Optional[list|str]=None, # HTMX extension names to include 50 | canonical:bool=True, # Automatically include canonical link? 51 | secret_key:Optional[str]=None, # Signing key for sessions 52 | key_fname:str='.sesskey', # Session cookie signing key file name 53 | session_cookie:str='session_', # Session cookie name 54 | max_age:int=365*24*3600, # Session cookie expiry time 55 | sess_path:str='/', # Session cookie path 56 | same_site:str='lax', # Session cookie same site policy 57 | sess_https_only:bool=False, # Session cookie HTTPS only? 58 | sess_domain:Optional[str]=None, # Session cookie domain 59 | htmlkw:Optional[dict]=None, # Attrs to add to the HTML tag 60 | bodykw:Optional[dict]=None, # Attrs to add to the Body tag 61 | reload_attempts:Optional[int]=1, # Number of reload attempts when live reloading 62 | reload_interval:Optional[int]=1000, # Time between reload attempts in ms 63 | static_path:str=".", # Where the static file route points to, defaults to root dir 64 | body_wrap:callable=noop_body, # FT wrapper for body contents 65 | nb_hdrs:bool=False, # If in notebook include headers inject headers in notebook DOM? 66 | **kwargs): 67 | "Create a FastHTML or FastHTMLWithLiveReload app." 68 | h = (picolink,) if pico or (pico is None and default_hdrs) else () 69 | if hdrs: h += tuple(hdrs) 70 | 71 | app = _app_factory(hdrs=h, ftrs=ftrs, before=before, middleware=middleware, live=live, debug=debug, title=title, routes=routes, exception_handlers=exception_handlers, 72 | on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, default_hdrs=default_hdrs, secret_key=secret_key, canonical=canonical, 73 | session_cookie=session_cookie, max_age=max_age, sess_path=sess_path, same_site=same_site, sess_https_only=sess_https_only, 74 | sess_domain=sess_domain, key_fname=key_fname, exts=exts, surreal=surreal, htmx=htmx, htmlkw=htmlkw, 75 | reload_attempts=reload_attempts, reload_interval=reload_interval, body_wrap=body_wrap, nb_hdrs=nb_hdrs, **(bodykw or {})) 76 | app.static_route_exts(static_path=static_path) 77 | if not db_file: return app,app.route 78 | 79 | db = database(db_file) 80 | if not tbls: tbls={} 81 | if kwargs: 82 | if isinstance(first(kwargs.values()), dict): tbls = kwargs 83 | else: 84 | kwargs['render'] = render 85 | tbls['items'] = kwargs 86 | dbtbls = [_get_tbl(db.t, k, v) for k,v in tbls.items()] 87 | if len(dbtbls)==1: dbtbls=dbtbls[0] 88 | return app,app.route,*dbtbls 89 | 90 | -------------------------------------------------------------------------------- /fasthtml/js.py: -------------------------------------------------------------------------------- 1 | """Basic external Javascript lib wrappers""" 2 | 3 | # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/03_js.ipynb. 4 | 5 | # %% auto 0 6 | __all__ = ['marked_imp', 'npmcdn', 'light_media', 'dark_media', 'MarkdownJS', 'KatexMarkdownJS', 'HighlightJS', 'SortableJS', 7 | 'MermaidJS'] 8 | 9 | # %% ../nbs/api/03_js.ipynb 10 | import re 11 | from fastcore.utils import * 12 | from fasthtml.components import * 13 | from fasthtml.xtend import * 14 | 15 | # %% ../nbs/api/03_js.ipynb 16 | def light_media( 17 | css: str # CSS to be included in the light media query 18 | ): 19 | "Render light media for day mode views" 20 | return Style('@media (prefers-color-scheme: light) {%s}' %css) 21 | 22 | # %% ../nbs/api/03_js.ipynb 23 | def dark_media( 24 | css: str # CSS to be included in the dark media query 25 | ): 26 | "Render dark media for night mode views" 27 | return Style('@media (prefers-color-scheme: dark) {%s}' %css) 28 | 29 | # %% ../nbs/api/03_js.ipynb 30 | marked_imp = """import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; 31 | """ 32 | npmcdn = 'https://cdn.jsdelivr.net/npm/' 33 | 34 | # %% ../nbs/api/03_js.ipynb 35 | def MarkdownJS( 36 | sel='.marked' # CSS selector for markdown elements 37 | ): 38 | "Implements browser-based markdown rendering." 39 | src = "proc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));" % sel 40 | return Script(marked_imp+src, type='module') 41 | 42 | # %% ../nbs/api/03_js.ipynb 43 | def KatexMarkdownJS( 44 | sel='.marked', # CSS selector for markdown elements 45 | inline_delim='$', # Delimiter for inline math 46 | display_delim='$$', # Delimiter for long math 47 | math_envs=None # List of environments to render as display math 48 | ): 49 | math_envs = math_envs or ['equation', 'align', 'gather', 'multline'] 50 | env_list = '[' + ','.join(f"'{env}'" for env in math_envs) + ']' 51 | fn = Path(__file__).parent/'katex.js' 52 | scr = ScriptX(fn, display_delim=re.escape(display_delim), inline_delim=re.escape(inline_delim), 53 | sel=sel, env_list=env_list, type='module') 54 | css = Link(rel="stylesheet", href=npmcdn+"katex@0.16.11/dist/katex.min.css") 55 | return scr,css 56 | 57 | # %% ../nbs/api/03_js.ipynb 58 | def HighlightJS( 59 | sel='pre code:not([data-highlighted="yes"])', # CSS selector for code elements. Default is industry standard, be careful before adjusting it 60 | langs:str|list|tuple='python', # Language(s) to highlight 61 | light='atom-one-light', # Light theme 62 | dark='atom-one-dark' # Dark theme 63 | ): 64 | "Implements browser-based syntax highlighting. Usage example [here](/tutorials/quickstart_for_web_devs.html#code-highlighting)." 65 | src = """ 66 | hljs.addPlugin(new CopyButtonPlugin()); 67 | hljs.configure({'cssSelector': '%s'}); 68 | htmx.onLoad(hljs.highlightAll);""" % sel 69 | hjs = 'highlightjs','cdn-release', 'build' 70 | hjc = 'arronhunt' ,'highlightjs-copy', 'dist' 71 | if isinstance(langs, str): langs = [langs] 72 | langjs = [jsd(*hjs, f'languages/{lang}.min.js') for lang in langs] 73 | return [jsd(*hjs, f'styles/{dark}.css', typ='css', media="(prefers-color-scheme: dark)"), 74 | jsd(*hjs, f'styles/{light}.css', typ='css', media="(prefers-color-scheme: light)"), 75 | jsd(*hjs, f'highlight.min.js'), 76 | jsd(*hjc, 'highlightjs-copy.min.js'), 77 | jsd(*hjc, 'highlightjs-copy.min.css', typ='css'), 78 | *langjs, Script(src, type='module')] 79 | 80 | # %% ../nbs/api/03_js.ipynb 81 | def SortableJS( 82 | sel='.sortable', # CSS selector for sortable elements 83 | ghost_class='blue-background-class' # When an element is being dragged, this is the class used to distinguish it from the rest 84 | ): 85 | src = """ 86 | import {Sortable} from 'https://cdn.jsdelivr.net/npm/sortablejs/+esm'; 87 | proc_htmx('%s', Sortable.create); 88 | """ % sel 89 | return Script(src, type='module') 90 | 91 | # %% ../nbs/api/03_js.ipynb 92 | def MermaidJS( 93 | sel='.language-mermaid', # CSS selector for mermaid elements 94 | theme='base', # Mermaid theme to use 95 | ): 96 | "Implements browser-based Mermaid diagram rendering." 97 | src = """ 98 | import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'; 99 | 100 | mermaid.initialize({ 101 | startOnLoad: false, 102 | theme: '%s', 103 | securityLevel: 'loose', 104 | flowchart: { useMaxWidth: false, useMaxHeight: false } 105 | }); 106 | 107 | function renderMermaidDiagrams(element, index) { 108 | try { 109 | const graphDefinition = element.textContent; 110 | const graphId = `mermaid-diagram-${index}`; 111 | mermaid.render(graphId, graphDefinition) 112 | .then(({svg, bindFunctions}) => { 113 | element.innerHTML = svg; 114 | bindFunctions?.(element); 115 | }) 116 | .catch(error => { 117 | console.error(`Error rendering Mermaid diagram ${index}:`, error); 118 | element.innerHTML = `Error rendering diagram: ${error.message}
`; 119 | }); 120 | } catch (error) { 121 | console.error(`Error processing Mermaid diagram ${index}:`, error); 122 | } 123 | } 124 | 125 | // Assuming proc_htmx is a function that triggers rendering 126 | proc_htmx('%s', renderMermaidDiagrams); 127 | """ % (theme, sel) 128 | return Script(src, type='module') 129 | 130 | -------------------------------------------------------------------------------- /nbs/ref/best_practice.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "FastHTML Best Practices" 3 | --- 4 | 5 | FastHTML applications are different to applications using FastAPI/react, Django, etc. Don't assume that FastHTML best practices are the same as those for other frameworks. Best practices embody the fast.ai philosophy: remove ceremony, leverage smart defaults, and write code that's both concise and clear. The following are some particular opportunities that both humans and language models sometimes miss: 6 | 7 | ## Database Table Creation 8 | 9 | **Before:** 10 | 11 | ```python 12 | todos = db.t.todos 13 | if not todos.exists(): 14 | todos.create(id=int, task=str, completed=bool, created=str, pk='id') 15 | ``` 16 | 17 | **After:** 18 | 19 | ```python 20 | class Todo: id:int; task:str; completed:bool; created:str 21 | todos = db.create(Todo) 22 | ``` 23 | 24 | FastLite's `create()` is idempotent - it creates the table if needed and returns the table object either way. Using a dataclass-style definition is cleaner and more Pythonic. The `id` field is automatically the primary key. 25 | 26 | ## Route Naming Conventions 27 | 28 | **Before:** 29 | 30 | ```python 31 | @rt("/") 32 | def get(): return Titled("Todo List", ...) 33 | 34 | @rt("/add") 35 | def post(task: str): ... 36 | ``` 37 | 38 | **After:** 39 | 40 | ```python 41 | @rt 42 | def index(): return Titled("Todo List", ...) # Special name for "/" 43 | @rt 44 | def add(task: str): ... # Function name becomes route 45 | ``` 46 | 47 | Use `@rt` without arguments and let the function name define the route. The special name `index` maps to `/`. 48 | 49 | ## Query Parameters over Path Parameters 50 | 51 | **Before:** 52 | 53 | ```python 54 | @rt("/toggle/{todo_id}") 55 | def post(todo_id: int): ... 56 | # URL: /toggle/123 57 | ``` 58 | 59 | **After:** 60 | 61 | ```python 62 | @rt 63 | def toggle(id: int): ... 64 | # URL: /toggle?id=123 65 | ``` 66 | 67 | Query parameters are more idiomatic in FastHTML and avoid duplicating param names in the path. 68 | 69 | ## Leverage Return Values 70 | 71 | :::{.column-body-outset} 72 | 73 | **Before:** 74 | 75 | ```python 76 | @rt 77 | def add(task: str): 78 | new_todo = todos.insert(task=task, completed=False, created=datetime.now().isoformat()) 79 | return todo_item(todos[new_todo]) 80 | 81 | @rt 82 | def toggle(id: int): 83 | todo = todos[id] 84 | todos.update(completed=not todo.completed, id=id) 85 | return todo_item(todos[id]) 86 | ``` 87 | 88 | **After:** 89 | 90 | ```python 91 | @rt 92 | def add(task: str): 93 | return todo_item(todos.insert(task=task, completed=False, created=datetime.now().isoformat())) 94 | 95 | @rt 96 | def toggle(id: int): 97 | return todo_item(todos.update(completed=not todos[id].completed, id=id)) 98 | ``` 99 | 100 | Both `insert()` and `update()` return the affected object, enabling functional chaining. 101 | 102 | ::: 103 | 104 | ## Use `.to()` for URL Generation 105 | 106 | **Before:** 107 | 108 | ```python 109 | hx_post=f"/toggle?id={todo.id}" 110 | ``` 111 | 112 | **After:** 113 | 114 | ```python 115 | hx_post=toggle.to(id=todo.id) 116 | ``` 117 | 118 | The `.to()` method generates URLs with type safety and is refactoring-friendly. 119 | 120 | ## PicoCSS comes free 121 | 122 | **Before:** 123 | 124 | ```python 125 | style = Style(""" 126 | .todo-container { max-width: 600px; margin: 0 auto; padding: 20px; } 127 | /* ... many more lines ... */ 128 | """) 129 | ``` 130 | 131 | **After:** 132 | 133 | ```python 134 | # Just use semantic HTML - Pico styles it automatically 135 | Container(...), Article(...), Card(...), Group(...) 136 | ``` 137 | 138 | `fast_app()` includes PicoCSS by default. Use semantic HTML elements that Pico styles automatically. Use MonsterUI (like shadcn, but for FastHTML) for more complex UI needs. 139 | 140 | ## Smart Defaults 141 | 142 | **Before:** 143 | 144 | ```python 145 | return Titled("Todo List", Container(...)) 146 | 147 | if __name__ == "__main__": 148 | serve() 149 | ``` 150 | 151 | **After:** 152 | 153 | ```python 154 | return Titled("Todo List", ...) # Container is automatic 155 | 156 | serve() # No need for if __name__ guard 157 | ``` 158 | 159 | `Titled` already wraps content in a `Container`, and `serve()` handles the main check internally. 160 | 161 | ## FastHTML Handles Iterables 162 | 163 | **Before:** 164 | 165 | ```python 166 | Section(*[todo_item(todo) for todo in all_todos], id="todo-list") 167 | ``` 168 | 169 | **After:** 170 | 171 | ```python 172 | Section(map(todo_item, all_todos), id="todo-list") 173 | ``` 174 | 175 | FastHTML components accept iterables directly - no need to unpack with `*`. 176 | 177 | ## Functional Patterns 178 | 179 | List comprehensions are great, but `map()` is often cleaner for simple transformations, especially when combined with FastHTML's iterable handling. 180 | 181 | ## Minimal Code 182 | 183 | **Before:** 184 | 185 | ```python 186 | @rt 187 | def delete(id: int): 188 | # Delete from database 189 | todos.delete(id) 190 | # Return empty response 191 | return "" 192 | ``` 193 | 194 | **After:** 195 | 196 | ```python 197 | @rt 198 | def delete(id: int): todos.delete(id) 199 | ``` 200 | 201 | - Skip comments when code is self-documenting 202 | - Don't return empty strings - `None` is returned by default 203 | - Use a single line for a single idea. 204 | 205 | ## Use POST for All Mutations 206 | 207 | **Before:** 208 | 209 | ```python 210 | hx_delete=f"/delete?id={todo.id}" 211 | ``` 212 | 213 | **After:** 214 | 215 | ```python 216 | hx_post=delete.to(id=todo.id) 217 | ``` 218 | 219 | FastHTML routes handle only GET and POST by default. Using only these two verbs is more idiomatic and simpler. 220 | 221 | ## Modern HTMX Event Syntax 222 | 223 | **Before:** 224 | 225 | ```python 226 | hx_on="htmx:afterRequest: this.reset()" 227 | ``` 228 | 229 | **After:** 230 | 231 | ```python 232 | hx_on__after_request="this.reset()" 233 | ``` 234 | 235 | This works because: 236 | 237 | - `hx-on="event: code"` is deprecated; `hx-on-event="code"` is preferred 238 | - FastHTML converts `_` to `-` (so `hx_on__after_request` becomes `hx-on--after-request`) 239 | - `::` in HTMX can be used as a shortcut for `:htmx:`. 240 | - HTMX natively accepts `-` instead of `:` (so `-htmx-` works like `:htmx:`) 241 | - HTMX accepts e.g `after-request` as an alternative to camelCase `afterRequest` 242 | 243 | -------------------------------------------------------------------------------- /nbs/tutorials/jupyter_and_fasthtml.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "2a2294a7", 6 | "metadata": {}, 7 | "source": [ 8 | "# Using Jupyter to write FastHTML\n", 9 | "\n", 10 | "> Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications.\n", 11 | "\n", 12 | "- skip_exec: true" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "4b1ee344", 18 | "metadata": {}, 19 | "source": [ 20 | "Writing FastHTML applications in Jupyter notebooks requires a slightly different process than normal Python applications. \n", 21 | "\n", 22 | ":::{.callout-tip title='Download this notebook and try it yourself'}\n", 23 | "The source code for this page is a [Jupyter notebook](https://github.com/AnswerDotAI/fasthtml/blob/main/nbs/tutorials/jupyter_and_fasthtml.ipynb). That makes it easy to directly experiment with it. However, as this is working code that means we have to comment out a few things in order for the documentation to build.\n", 24 | ":::\n", 25 | "\n", 26 | "The first step is to import necessary libraries. As using FastHTML inside a Jupyter notebook is a special case, it remains a special import. " 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "e244a34a", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "from fasthtml.common import *\n", 37 | "from fasthtml.jupyter import JupyUvi, HTMX" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "7f38039a", 43 | "metadata": {}, 44 | "source": [ 45 | "Let's create an app with `fast_app`." 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "id": "9ac00db7", 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "app, rt = fast_app(pico=True)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "d24f2972", 61 | "metadata": {}, 62 | "source": [ 63 | "Define a route to test the application." 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "id": "ea60bc88", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "@rt\n", 74 | "def index():\n", 75 | " return Titled('Hello, Jupyter',\n", 76 | " P('Welcome to the FastHTML + Jupyter example'),\n", 77 | " Button('Click', hx_get='/click', hx_target='#dest'),\n", 78 | " Div(id='dest')\n", 79 | " )" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "e512dbab", 85 | "metadata": {}, 86 | "source": [ 87 | "Create a `server` object using `JupyUvi`, which also starts Uvicorn. The `server` runs in a separate thread from Jupyter, so it can use normal HTTP client functions in a notebook." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "id": "3b815c60", 94 | "metadata": {}, 95 | "outputs": [ 96 | { 97 | "data": { 98 | "text/html": [ 99 | "\n", 100 | "" 107 | ], 108 | "text/plain": [ 109 | "