diff --git a/examples/adv_app.py b/examples/adv_app.py index d074b8f8..c869d6f3 100644 --- a/examples/adv_app.py +++ b/examples/adv_app.py @@ -58,7 +58,6 @@ def before(req, sess): markdown_js = """ import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; -import { proc_htmx} from "https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js/fasthtml.js"; proc_htmx('.markdown', e => e.innerHTML = marked.parse(e.textContent)); """ diff --git a/fasthtml/_modidx.py b/fasthtml/_modidx.py index 7406b1fd..f7d779c0 100644 --- a/fasthtml/_modidx.py +++ b/fasthtml/_modidx.py @@ -104,11 +104,10 @@ 'fasthtml.core.str2date': ('api/core.html#str2date', 'fasthtml/core.py'), 'fasthtml.core.str2int': ('api/core.html#str2int', 'fasthtml/core.py'), 'fasthtml.core.uri': ('api/core.html#uri', 'fasthtml/core.py')}, - 'fasthtml.fastapp': { 'fasthtml.fastapp.ContainerX': ('api/fastapp.html#containerx', 'fasthtml/fastapp.py'), - 'fasthtml.fastapp.PageX': ('api/fastapp.html#pagex', 'fasthtml/fastapp.py'), - 'fasthtml.fastapp._app_factory': ('api/fastapp.html#_app_factory', 'fasthtml/fastapp.py'), + 'fasthtml.fastapp': { 'fasthtml.fastapp._app_factory': ('api/fastapp.html#_app_factory', 'fasthtml/fastapp.py'), 'fasthtml.fastapp._get_tbl': ('api/fastapp.html#_get_tbl', 'fasthtml/fastapp.py'), - 'fasthtml.fastapp.fast_app': ('api/fastapp.html#fast_app', 'fasthtml/fastapp.py')}, + 'fasthtml.fastapp.fast_app': ('api/fastapp.html#fast_app', 'fasthtml/fastapp.py'), + 'fasthtml.fastapp.jupy_app': ('api/fastapp.html#jupy_app', 'fasthtml/fastapp.py')}, 'fasthtml.ft': {}, 'fasthtml.js': { 'fasthtml.js.HighlightJS': ('api/js.html#highlightjs', 'fasthtml/js.py'), 'fasthtml.js.KatexMarkdownJS': ('api/js.html#katexmarkdownjs', 'fasthtml/js.py'), @@ -116,6 +115,16 @@ 'fasthtml.js.SortableJS': ('api/js.html#sortablejs', 'fasthtml/js.py'), 'fasthtml.js.dark_media': ('api/js.html#dark_media', 'fasthtml/js.py'), 'fasthtml.js.light_media': ('api/js.html#light_media', 'fasthtml/js.py')}, + 'fasthtml.jupyter': { 'fasthtml.jupyter.FastJupy': ('api/jupyter.html#fastjupy', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.HTMX': ('api/jupyter.html#htmx', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.JupyUvi': ('api/jupyter.html#jupyuvi', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.JupyUvi.__init__': ('api/jupyter.html#jupyuvi.__init__', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.JupyUvi.start': ('api/jupyter.html#jupyuvi.start', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.JupyUvi.stop': ('api/jupyter.html#jupyuvi.stop', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.is_port_free': ('api/jupyter.html#is_port_free', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.nb_serve': ('api/jupyter.html#nb_serve', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.nb_serve_async': ('api/jupyter.html#nb_serve_async', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.wait_port_free': ('api/jupyter.html#wait_port_free', 'fasthtml/jupyter.py')}, 'fasthtml.live_reload': {}, 'fasthtml.oauth': { 'fasthtml.oauth.DiscordAppClient': ('api/oauth.html#discordappclient', 'fasthtml/oauth.py'), 'fasthtml.oauth.DiscordAppClient.__init__': ( 'api/oauth.html#discordappclient.__init__', diff --git a/fasthtml/core.pyi b/fasthtml/core.pyi index 29d1ed1d..a67600a6 100644 --- a/fasthtml/core.pyi +++ b/fasthtml/core.pyi @@ -220,6 +220,9 @@ class RouterX(Router): def __init__(self, app, routes=None, redirect_slashes=True, default=None, *, middleware=None): ... + def _add_route(self, route): + ... + def add_route(self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True): ... @@ -227,7 +230,7 @@ class RouterX(Router): ... htmxsrc = Script(src='https://unpkg.com/htmx.org@next/dist/htmx.min.js') htmxwssrc = Script(src='https://unpkg.com/htmx-ext-ws/ws.js') -fhjsscr = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@main/fasthtml.js') +fhjsscr = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.4/fasthtml.js') htmxctsrc = Script(src='https://unpkg.com/htmx-ext-transfer-encoding-chunked/transfer-encoding-chunked.js') surrsrc = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js') scopesrc = Script(src='https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js') diff --git a/fasthtml/fastapp.py b/fasthtml/fastapp.py index 1821439a..64dae355 100644 --- a/fasthtml/fastapp.py +++ b/fasthtml/fastapp.py @@ -12,9 +12,10 @@ from .pico import * from .starlette import * from .live_reload import FastHTMLWithLiveReload +from .jupyter import _iframe_scr, cors_allow # %% auto 0 -__all__ = ['fast_app', 'ContainerX', 'PageX'] +__all__ = ['fast_app', 'jupy_app'] # %% ../nbs/api/10_fastapp.ipynb def _get_tbl(dt, nm, schema): @@ -93,5 +94,8 @@ def fast_app( return app,app.route,*dbtbls # %% ../nbs/api/10_fastapp.ipynb -def ContainerX(*cs, **kwargs): return Main(*cs, **kwargs, cls='container', hx_push_url='true', hx_swap_oob='true', id='main') -def PageX(title, *con): return Title(title), ContainerX(H1(title), *con) +def jupy_app(pico=False, hdrs=None, middleware=None, **kwargs): + "Same as `fast_app` but for Jupyter notebooks" + hdrs = listify(hdrs)+[_iframe_scr] + middleware = listify(middleware)+[cors_allow] + return fast_app(pico=pico, hdrs=hdrs, middleware=middleware, **kwargs) diff --git a/fasthtml/js.py b/fasthtml/js.py index 427a4676..a79a8496 100644 --- a/fasthtml/js.py +++ b/fasthtml/js.py @@ -27,7 +27,6 @@ def dark_media( # %% ../nbs/api/03_js.ipynb marked_imp = """import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; - import { proc_htmx } from "https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.3/fasthtml.js"; """ npmcdn = 'https://cdn.jsdelivr.net/npm/' @@ -85,7 +84,6 @@ def SortableJS( ): src = """ import {Sortable} from 'https://cdn.jsdelivr.net/npm/sortablejs/+esm'; -import {proc_htmx} from "https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.3/fasthtml.js"; proc_htmx('%s', Sortable.create); """ % sel return Script(src, type='module') diff --git a/fasthtml/jupyter.py b/fasthtml/jupyter.py new file mode 100644 index 00000000..302e029b --- /dev/null +++ b/fasthtml/jupyter.py @@ -0,0 +1,99 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/06_jupyter.ipynb. + +# %% auto 0 +__all__ = ['cors_allow', 'nb_serve', 'nb_serve_async', 'is_port_free', 'wait_port_free', 'JupyUvi', 'FastJupy', 'HTMX'] + +# %% ../nbs/api/06_jupyter.ipynb +import asyncio, socket, time, uvicorn +from threading import Thread +from fastcore.utils import * +from .core import * +from .components import * +from .xtend import * +from IPython.display import HTML,Markdown,IFrame +from starlette.middleware.cors import CORSMiddleware +from starlette.middleware import Middleware +from fastcore.parallel import startthread + +# %% ../nbs/api/06_jupyter.ipynb +def nb_serve(app, log_level="error", port=8000, **kwargs): + "Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`" + server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, port=port, **kwargs)) + async def async_run_server(server): await server.serve() + @startthread + def run_server(): asyncio.run(async_run_server(server)) + while not server.started: time.sleep(0.01) + return server + +# %% ../nbs/api/06_jupyter.ipynb +async def nb_serve_async(app, log_level="error", port=8000, **kwargs): + "Async version of `nb_serve`" + server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, port=port, **kwargs)) + asyncio.get_running_loop().create_task(server.serve()) + while not server.started: await asyncio.sleep(0.01) + return server + +# %% ../nbs/api/06_jupyter.ipynb +def is_port_free(port, host='localhost'): + "Check if `port` is free on `host`" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((host, port)) + return True + except OSError: return False + finally: sock.close() + +# %% ../nbs/api/06_jupyter.ipynb +def wait_port_free(port, host='localhost', max_wait=3): + "Wait for `port` to be free on `host`" + start_time = time.time() + while not is_port_free(port): + if time.time() - start_time>max_wait: return print(f"Timeout") + time.sleep(0.1) + +# %% ../nbs/api/06_jupyter.ipynb +cors_allow = Middleware(CORSMiddleware, allow_credentials=True, + allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) + +# %% ../nbs/api/06_jupyter.ipynb +class JupyUvi: + "Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`" + def __init__(self, app, log_level="error", port=8000, start=True, **kwargs): + self.kwargs = kwargs + store_attr(but='start') + self.server = None + if start: self.start() + + def start(self): + self.server = nb_serve(self.app, log_level=self.log_level, port=self.port, **self.kwargs) + + def stop(self): + self.server.should_exit = True + wait_port_free(self.port) + +# %% ../nbs/api/06_jupyter.ipynb +# The script lets an iframe parent know of changes so that it can resize automatically. +_iframe_scr = Script(""" + function sendmsg() {window.parent.postMessage({height: document.documentElement.offsetHeight}, '*')} + window.onload = function() { + sendmsg(); + document.body.addEventListener('htmx:afterSettle', sendmsg); + };""") + +# %% ../nbs/api/06_jupyter.ipynb +def FastJupy(hdrs=None, middleware=None, **kwargs): + "Same as FastHTML, but with Jupyter compatible middleware and headers added" + hdrs = listify(hdrs)+[_iframe_scr] + middleware = listify(middleware)+[cors_allow] + return FastHTML(hdrs=hdrs, middleware=middleware, **kwargs) + +# %% ../nbs/api/06_jupyter.ipynb +def HTMX(host='localhost', port=8000): + "An iframe which displays the HTMX application in a notebook." + return HTML(f' """) diff --git a/fasthtml/katex.js b/fasthtml/katex.js index e005fe4f..128a6fc6 100644 --- a/fasthtml/katex.js +++ b/fasthtml/katex.js @@ -1,5 +1,4 @@ import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; -import { proc_htmx } from "https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.3/fasthtml.js"; import katex from "https://cdn.jsdelivr.net/npm/katex/dist/katex.mjs"; const renderMath = (tex, displayMode) => { return katex.renderToString(tex, { diff --git a/fasthtml/xtend.pyi b/fasthtml/xtend.pyi index 9adfe0d9..0e2ffa07 100644 --- a/fasthtml/xtend.pyi +++ b/fasthtml/xtend.pyi @@ -1,5 +1,5 @@ """Simple extensions to standard HTML components, such as adding sensible defaults""" -__all__ = ['sid_scr', 'A', 'Form', 'AX', 'Hidden', 'CheckboxX', 'Script', 'Style', 'double_braces', 'undouble_braces', 'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'On', 'Prev', 'Now', 'AnyNow', 'run_js', 'HtmxOn', 'Titled', 'Socials', 'Favicon', 'jsd', 'clear'] +__all__ = ['sid_scr', 'A', 'AX', 'Form', 'Hidden', 'CheckboxX', 'Script', 'Style', 'double_braces', 'undouble_braces', 'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'Surreal', 'On', 'Prev', 'Now', 'AnyNow', 'run_js', 'HtmxOn', 'jsd', 'Titled', 'Socials', 'Favicon', 'clear'] from dataclasses import dataclass, asdict from typing import Any from fastcore.utils import * @@ -16,14 +16,14 @@ def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', hx_vals=None, id= """An A tag; `href` defaults to '#' for more concise use with HTMX""" ... -def Form(*c, enctype='multipart/form-data', target_id=None, hx_vals=None, id=None, cls=None, title=None, style=None, accesskey=None, contenteditable=None, dir=None, draggable=None, enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None, popover=None, spellcheck=None, tabindex=None, translate=None, hx_get=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, hx_select_oob=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, **kwargs) -> FT: - """A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'`""" - ... - def AX(txt, hx_get=None, target_id=None, hx_swap=None, href='#', *, hx_vals=None, id=None, cls=None, title=None, style=None, accesskey=None, contenteditable=None, dir=None, draggable=None, enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None, popover=None, spellcheck=None, tabindex=None, translate=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_swap_oob=None, hx_include=None, hx_select=None, hx_select_oob=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, **kwargs) -> FT: """An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params""" ... +def Form(*c, enctype='multipart/form-data', target_id=None, hx_vals=None, id=None, cls=None, title=None, style=None, accesskey=None, contenteditable=None, dir=None, draggable=None, enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None, popover=None, spellcheck=None, tabindex=None, translate=None, hx_get=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, hx_select_oob=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, **kwargs) -> FT: + """A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'`""" + ... + def Hidden(value: Any='', id: Any=None, *, target_id=None, hx_vals=None, cls=None, title=None, style=None, accesskey=None, contenteditable=None, dir=None, draggable=None, enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None, popover=None, spellcheck=None, tabindex=None, translate=None, hx_get=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, hx_select_oob=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, **kwargs) -> FT: """An Input of type 'hidden'""" ... @@ -64,8 +64,12 @@ def StyleX(fname, **kw): """A `style` element with contents read from `fname` and variables replaced from `kw`""" ... +def Surreal(code: str): + """Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')`""" + ... + def On(code: str, event: str='click', sel: str='', me=True): - """An async surreal.js script block event handler for `event` on selector `sel`, making available parent `p`, event `ev`, and target `e`""" + """An async surreal.js script block event handler for `event` on selector `sel,p`, making available parent `p`, event `ev`, and target `e`""" ... def Prev(code: str, event: str='click'): @@ -87,6 +91,10 @@ def run_js(js, id=None, **kw): def HtmxOn(eventname: str, code: str): ... +def jsd(org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, **kwargs) -> FT: + """jsdelivr `Script` or CSS `Link` tag, or URL""" + ... + def Titled(title: str='FastHTML app', *args, cls='container', target_id=None, hx_vals=None, id=None, style=None, accesskey=None, contenteditable=None, dir=None, draggable=None, enterkeyhint=None, hidden=None, inert=None, inputmode=None, lang=None, popover=None, spellcheck=None, tabindex=None, translate=None, hx_get=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_swap=None, hx_swap_oob=None, hx_include=None, hx_select=None, hx_select_oob=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, **kwargs) -> FT: """An HTML partial containing a `Title`, and `H1`, and any provided children""" ... @@ -99,10 +107,6 @@ def Favicon(light_icon, dark_icon): """Light and dark favicon headers""" ... -def jsd(org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, **kwargs) -> FT: - """jsdelivr `Script` or CSS `Link` tag, or URL""" - ... - def clear(id): ... sid_scr = Script('\nfunction uuid() {\n return [...crypto.getRandomValues(new Uint8Array(10))].map(b=>b.toString(36)).join(\'\');\n}\n\nsessionStorage.setItem("sid", sessionStorage.getItem("sid") || uuid());\n\nhtmx.on("htmx:configRequest", (e) => {\n const sid = sessionStorage.getItem("sid");\n if (sid) {\n const url = new URL(e.detail.path, window.location.origin);\n url.searchParams.set(\'sid\', sid);\n e.detail.path = url.pathname + url.search;\n }\n});\n') \ No newline at end of file diff --git a/nbs/api/03_js.ipynb b/nbs/api/03_js.ipynb index ca8468db..496bdeb1 100644 --- a/nbs/api/03_js.ipynb +++ b/nbs/api/03_js.ipynb @@ -129,7 +129,6 @@ "source": [ "#| export\n", "marked_imp = \"\"\"import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n", - " import { proc_htmx } from \"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.3/fasthtml.js\";\n", "\"\"\"\n", "npmcdn = 'https://cdn.jsdelivr.net/npm/'" ] @@ -265,7 +264,6 @@ " ):\n", " src = \"\"\"\n", "import {Sortable} from 'https://cdn.jsdelivr.net/npm/sortablejs/+esm';\n", - "import {proc_htmx} from \"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.3/fasthtml.js\";\n", "proc_htmx('%s', Sortable.create);\n", "\"\"\" % sel\n", " return Script(src, type='module')" diff --git a/nbs/api/06_jupyter.ipynb b/nbs/api/06_jupyter.ipynb new file mode 100644 index 00000000..d21b4a38 --- /dev/null +++ b/nbs/api/06_jupyter.ipynb @@ -0,0 +1,383 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7832bb1a", + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp jupyter" + ] + }, + { + "cell_type": "markdown", + "id": "257b6e4b", + "metadata": {}, + "source": [ + "# Jupyter compatibility\n", + "\n", + "- Use FastHTML in Jupyter notebooks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c69d9d0", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "import asyncio, socket, time, uvicorn\n", + "from threading import Thread\n", + "from fastcore.utils import *\n", + "from fasthtml.core import *\n", + "from fasthtml.components import *\n", + "from fasthtml.xtend import *\n", + "from IPython.display import HTML,Markdown,IFrame\n", + "from starlette.middleware.cors import CORSMiddleware\n", + "from starlette.middleware import Middleware\n", + "from fastcore.parallel import startthread" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1c7b207", + "metadata": {}, + "outputs": [], + "source": [ + "from httpx import get, AsyncClient" + ] + }, + { + "cell_type": "markdown", + "id": "fab2984f", + "metadata": {}, + "source": [ + "## Helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5d3a8f7", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def nb_serve(app, log_level=\"error\", port=8000, **kwargs):\n", + " \"Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`\"\n", + " server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, port=port, **kwargs))\n", + " async def async_run_server(server): await server.serve()\n", + " @startthread\n", + " def run_server(): asyncio.run(async_run_server(server))\n", + " while not server.started: time.sleep(0.01)\n", + " return server" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3242080c", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "async def nb_serve_async(app, log_level=\"error\", port=8000, **kwargs):\n", + " \"Async version of `nb_serve`\"\n", + " server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, port=port, **kwargs))\n", + " asyncio.get_running_loop().create_task(server.serve())\n", + " while not server.started: await asyncio.sleep(0.01)\n", + " return server" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "508917bc", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def is_port_free(port, host='localhost'):\n", + " \"Check if `port` is free on `host`\"\n", + " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", + " try:\n", + " sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n", + " sock.bind((host, port))\n", + " return True\n", + " except OSError: return False\n", + " finally: sock.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1779cb76", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def wait_port_free(port, host='localhost', max_wait=3):\n", + " \"Wait for `port` to be free on `host`\"\n", + " start_time = time.time()\n", + " while not is_port_free(port):\n", + " if time.time() - start_time>max_wait: return print(f\"Timeout\")\n", + " time.sleep(0.1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5c57eeb", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "cors_allow = Middleware(CORSMiddleware, allow_credentials=True,\n", + " allow_origins=[\"*\"], allow_methods=[\"*\"], allow_headers=[\"*\"])" + ] + }, + { + "cell_type": "markdown", + "id": "dcb6ada4", + "metadata": {}, + "source": [ + "## Using FastHTML in Jupyter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29a834a5", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class JupyUvi:\n", + " \"Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`\"\n", + " def __init__(self, app, log_level=\"error\", port=8000, start=True, **kwargs):\n", + " self.kwargs = kwargs\n", + " store_attr(but='start')\n", + " self.server = None\n", + " if start: self.start()\n", + "\n", + " def start(self):\n", + " self.server = nb_serve(self.app, log_level=self.log_level, port=self.port, **self.kwargs)\n", + "\n", + " def stop(self):\n", + " self.server.should_exit = True\n", + " wait_port_free(self.port)" + ] + }, + { + "cell_type": "markdown", + "id": "7c84a5f2", + "metadata": {}, + "source": [ + "Creating an object of this class also starts the Uvicorn server. It runs in a separate thread, so you can use normal HTTP client functions in a notebook. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f4b31e9", + "metadata": {}, + "outputs": [], + "source": [ + "app = FastHTML()\n", + "\n", + "@app.route\n", + "def index(): return 'hi'\n", + "\n", + "port = 8001\n", + "server = JupyUvi(app, port=port)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e57a6a1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'hi'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get(f'http://localhost:{port}').text" + ] + }, + { + "cell_type": "markdown", + "id": "8dd94a9a", + "metadata": {}, + "source": [ + "You can stop the server, modify routes, and start the server again without restarting the notebook or recreating the server or application. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5b33bfd", + "metadata": {}, + "outputs": [], + "source": [ + "server.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a928d459", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "# The script lets an iframe parent know of changes so that it can resize automatically. \n", + "_iframe_scr = Script(\"\"\"\n", + " function sendmsg() {window.parent.postMessage({height: document.documentElement.offsetHeight}, '*')}\n", + " window.onload = function() {\n", + " sendmsg();\n", + " document.body.addEventListener('htmx:afterSettle', sendmsg);\n", + " };\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00fb3d5e", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def FastJupy(hdrs=None, middleware=None, **kwargs):\n", + " \"Same as FastHTML, but with Jupyter compatible middleware and headers added\"\n", + " hdrs = listify(hdrs)+[_iframe_scr]\n", + " middleware = listify(middleware)+[cors_allow]\n", + " return FastHTML(hdrs=hdrs, middleware=middleware, **kwargs)" + ] + }, + { + "cell_type": "markdown", + "id": "0726c769", + "metadata": {}, + "source": [ + "Instead of using the FastHTML class, use the FastJupy class. It's a thin wrapper for FastHTML which adds the necessary headers and middleware required for Jupyter compatibility. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d520bab", + "metadata": {}, + "outputs": [], + "source": [ + "app = FastJupy()\n", + "rt = app.route\n", + "server = JupyUvi(app, port=port)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ef1415d", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def HTMX(host='localhost', port=8000):\n", + " \"An iframe which displays the HTMX application in a notebook.\"\n", + " return HTML(f' \"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf152565", + "metadata": {}, + "outputs": [], + "source": [ + "@rt\n", + "def index():\n", + " return Div(\n", + " P(A('Click me', hx_get=update, hx_target='#result')),\n", + " P(A('No me!', hx_get=update, hx_target='#result')),\n", + " Div(id='result'))\n", + "\n", + "@rt\n", + "def update(): return Div(P('Hi!'),P('There!'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0063bb43", + "metadata": {}, + "outputs": [], + "source": [ + "# Run the notebook locally to see the HTMX iframe in action\n", + "# HTMX()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfc3271d", + "metadata": {}, + "outputs": [], + "source": [ + "server.stop()" + ] + }, + { + "cell_type": "markdown", + "id": "474e14b4", + "metadata": {}, + "source": [ + "## Export -" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d211e8e2", + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "import nbdev; nbdev.nbdev_export()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77112b77", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nbs/api/10_fastapp.ipynb b/nbs/api/10_fastapp.ipynb index 4780db13..09d22acc 100644 --- a/nbs/api/10_fastapp.ipynb +++ b/nbs/api/10_fastapp.ipynb @@ -28,8 +28,8 @@ "\n", "app, rt = fast_app()\n", "\n", - "@rt('/')\n", - "def get(): return Titled(\"A demo of fast_app()@\")\n", + "@rt\n", + "def index(): return Titled(\"A demo of fast_app()@\")\n", "\n", "serve()\n", "```" @@ -50,7 +50,17 @@ "from fasthtml.basics import *\n", "from fasthtml.pico import *\n", "from fasthtml.starlette import *\n", - "from fasthtml.live_reload import FastHTMLWithLiveReload" + "from fasthtml.live_reload import FastHTMLWithLiveReload\n", + "from fasthtml.jupyter import _iframe_scr, cors_allow" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from fasthtml.jupyter import JupyUvi,HTMX" ] }, { @@ -157,8 +167,74 @@ "outputs": [], "source": [ "#| export\n", - "def ContainerX(*cs, **kwargs): return Main(*cs, **kwargs, cls='container', hx_push_url='true', hx_swap_oob='true', id='main')\n", - "def PageX(title, *con): return Title(title), ContainerX(H1(title), *con)" + "def jupy_app(pico=False, hdrs=None, middleware=None, **kwargs):\n", + " \"Same as `fast_app` but for Jupyter notebooks\"\n", + " hdrs = listify(hdrs)+[_iframe_scr]\n", + " middleware = listify(middleware)+[cors_allow]\n", + " return fast_app(pico=pico, hdrs=hdrs, middleware=middleware, **kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app,rt = jupy_app()\n", + "server = JupyUvi(app)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@rt\n", + "def index():\n", + " return Div(\n", + " P(A('Click me', hx_get=update, hx_target='#result')),\n", + " P(A('No me!', hx_get=update, hx_target='#result')),\n", + " Div(id='result'))\n", + "\n", + "@rt\n", + "def update(): return Div(P('Hi!'),P('There!'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Run the notebook locally to see the HTMX iframe in action\n", + "# HTMX()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "server.stop()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Export -" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "import nbdev; nbdev.nbdev_export()" ] }, { diff --git a/nbs/llms-ctx-full.txt b/nbs/llms-ctx-full.txt index f3d33b2a..e19c19c1 100644 --- a/nbs/llms-ctx-full.txt +++ b/nbs/llms-ctx-full.txt @@ -1232,7 +1232,7 @@ All other attributes available in htmx. |------------------------------------------------------|-------------| | [`HX-Location`](@/headers/hx-location.md) | allows you to do a client-side redirect that does not do a full page reload | [`HX-Push-Url`](@/headers/hx-push-url.md) | pushes a new url into the history stack -| `HX-Redirect` | can be used to do a client-side redirect to a new location +| [`HX-Redirect`](@/headers/hx-redirect.md) | can be used to do a client-side redirect to a new location | `HX-Refresh` | if set to "true" the client-side will do a full refresh of the page | [`HX-Replace-Url`](@/headers/hx-replace-url.md) | replaces the current URL in the location bar | `HX-Reswap` | allows you to specify how the response will be swapped. See [hx-swap](@/attributes/hx-swap.md) for possible values @@ -1360,15 +1360,16 @@ listed below: | `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent | | `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates | | `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated | -| `htmx.config.scrollBehavior` | defaults to 'instant', the behavior for a boosted link on page transitions. The allowed values are `auto`, `instant` and `smooth`. Instant will scroll instantly in a single jump, smooth will scroll smoothly, while auto will behave like a vanilla link. | +| `htmx.config.scrollBehavior` | defaults to 'instant', the scroll behavior when using the [show](@/attributes/hx-swap.md#scrolling-scroll-show) modifier with `hx-swap`. The allowed values are `instant` (scrolling should happen instantly in a single jump), `smooth` (scrolling should animate smoothly) and `auto` (scroll behavior is determined by the computed value of [scroll-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior)). | | `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. | | `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will append the target element to the `GET` request in the format `org.htmx.cache-buster=targetElementId` | | `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. | -| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body | +| `htmx.config.methodsThatUseUrlParams` | defaults to `["get", "delete"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body | | `htmx.config.selfRequestsOnly` | defaults to `true`, whether to only allow AJAX requests to the same domain as the current document | | `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content | | `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. | | `htmx.config.triggerSpecsCache` | defaults to `null`, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) | +| `htmx.config.responseHandling` | the default [Response Handling](@/docs.md#response-handling) behavior for response status codes can be configured here to either swap or error | | `htmx.config.allowNestedOobSwaps` | defaults to `true`, whether to process OOB swaps on elements that are nested within the main response element. See [Nested OOB Swaps](@/attributes/hx-swap-oob.md#nested-oob-swaps). | @@ -1704,8 +1705,8 @@ Append / Prepend elements. * 🔥 `me().insertAdjacentHTML("beforebegin", new_element)` AJAX (replace jQuery `ajax()`) -* Use [htmx](https://htmx.org/) or [htmz](https://leanrada.com/htmz/) or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [XMLHttpRequest()](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) directly. -* Using `fetch()` +* Use [htmx](https://htmx.org/) or [htmz](https://leanrada.com/htmz/) or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [XMLHttpRequest()](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) +* Example using `fetch()` ```js me().on("click", async event => { let e = me(event) @@ -1720,7 +1721,7 @@ me().on("click", async event => { catch (error) { console.warn(`fetch(): ${error}`) } }) ``` -* Using `XMLHttpRequest()` +* Example using `XMLHttpRequest()` ```js me().on("click", async event => { let e = me(event) @@ -2331,7 +2332,7 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): > The `FastHTML` subclass of `Starlette`, along with the `RouterX` and `RouteX` classes it automatically uses. -- `def date(s)` +- `def parsed_date(s)` Convert `s` to a datetime - `def snake2hyphens(s)` @@ -2344,6 +2345,9 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def str2int(s)` Convert `s` to an `int` +- `def str2date(s)` + `date.fromisoformat` with empty string handling + - `@dataclass class HttpHeader` - `def __init__(self, k, v)` @@ -2621,12 +2625,12 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `@delegates(ft_hx, keep=True) def A(*c, **kwargs)` An A tag; `href` defaults to '#' for more concise use with HTMX -- `@delegates(ft_hx, keep=True) def Form(*c, **kwargs)` - A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'` - - `@delegates(ft_hx, keep=True) def AX(txt, hx_get, target_id, hx_swap, href, **kwargs)` An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params +- `@delegates(ft_hx, keep=True) def Form(*c, **kwargs)` + A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'` + - `@delegates(ft_hx, keep=True) def Hidden(value, id, **kwargs)` An Input of type 'hidden' @@ -2657,8 +2661,11 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def StyleX(fname, **kw)` A `style` element with contents read from `fname` and variables replaced from `kw` +- `def Surreal(code)` + Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')` + - `def On(code, event, sel, me)` - An async surreal.js script block event handler for `event` on selector `sel`, making available parent `p`, event `ev`, and target `e` + An async surreal.js script block event handler for `event` on selector `sel,p`, making available parent `p`, event `ev`, and target `e` - `def Prev(code, event)` An async surreal.js script block event handler for `event` on previous sibling, with same vars as `On` @@ -2672,6 +2679,9 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def run_js(js, id, **kw)` Run `js` script, auto-generating `id` based on name of caller if needed, and js-escaping any `kw` params +- `def jsd(org, repo, root, path, prov, typ, ver, esm, **kwargs)` + jsdelivr `Script` or CSS `Link` tag, or URL + - `@delegates(ft_hx, keep=True) def Titled(title, *args, **kwargs)` An HTML partial containing a `Title`, and `H1`, and any provided children @@ -2680,9 +2690,6 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def Favicon(light_icon, dark_icon)` Light and dark favicon headers - -- `def jsd(org, repo, root, path, prov, typ, ver, esm, **kwargs)` - jsdelivr `Script` or CSS `Link` tag, or URL from asyncio import sleep from fasthtml.common import * diff --git a/nbs/llms-ctx.txt b/nbs/llms-ctx.txt index a27fe204..753a4da7 100644 --- a/nbs/llms-ctx.txt +++ b/nbs/llms-ctx.txt @@ -1232,7 +1232,7 @@ All other attributes available in htmx. |------------------------------------------------------|-------------| | [`HX-Location`](@/headers/hx-location.md) | allows you to do a client-side redirect that does not do a full page reload | [`HX-Push-Url`](@/headers/hx-push-url.md) | pushes a new url into the history stack -| `HX-Redirect` | can be used to do a client-side redirect to a new location +| [`HX-Redirect`](@/headers/hx-redirect.md) | can be used to do a client-side redirect to a new location | `HX-Refresh` | if set to "true" the client-side will do a full refresh of the page | [`HX-Replace-Url`](@/headers/hx-replace-url.md) | replaces the current URL in the location bar | `HX-Reswap` | allows you to specify how the response will be swapped. See [hx-swap](@/attributes/hx-swap.md) for possible values @@ -1360,15 +1360,16 @@ listed below: | `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent | | `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates | | `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated | -| `htmx.config.scrollBehavior` | defaults to 'instant', the behavior for a boosted link on page transitions. The allowed values are `auto`, `instant` and `smooth`. Instant will scroll instantly in a single jump, smooth will scroll smoothly, while auto will behave like a vanilla link. | +| `htmx.config.scrollBehavior` | defaults to 'instant', the scroll behavior when using the [show](@/attributes/hx-swap.md#scrolling-scroll-show) modifier with `hx-swap`. The allowed values are `instant` (scrolling should happen instantly in a single jump), `smooth` (scrolling should animate smoothly) and `auto` (scroll behavior is determined by the computed value of [scroll-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior)). | | `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. | | `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will append the target element to the `GET` request in the format `org.htmx.cache-buster=targetElementId` | | `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. | -| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body | +| `htmx.config.methodsThatUseUrlParams` | defaults to `["get", "delete"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body | | `htmx.config.selfRequestsOnly` | defaults to `true`, whether to only allow AJAX requests to the same domain as the current document | | `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content | | `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. | | `htmx.config.triggerSpecsCache` | defaults to `null`, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) | +| `htmx.config.responseHandling` | the default [Response Handling](@/docs.md#response-handling) behavior for response status codes can be configured here to either swap or error | | `htmx.config.allowNestedOobSwaps` | defaults to `true`, whether to process OOB swaps on elements that are nested within the main response element. See [Nested OOB Swaps](@/attributes/hx-swap-oob.md#nested-oob-swaps). | @@ -1704,8 +1705,8 @@ Append / Prepend elements. * 🔥 `me().insertAdjacentHTML("beforebegin", new_element)` AJAX (replace jQuery `ajax()`) -* Use [htmx](https://htmx.org/) or [htmz](https://leanrada.com/htmz/) or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [XMLHttpRequest()](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) directly. -* Using `fetch()` +* Use [htmx](https://htmx.org/) or [htmz](https://leanrada.com/htmz/) or [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [XMLHttpRequest()](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) +* Example using `fetch()` ```js me().on("click", async event => { let e = me(event) @@ -1720,7 +1721,7 @@ me().on("click", async event => { catch (error) { console.warn(`fetch(): ${error}`) } }) ``` -* Using `XMLHttpRequest()` +* Example using `XMLHttpRequest()` ```js me().on("click", async event => { let e = me(event) @@ -2331,7 +2332,7 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): > The `FastHTML` subclass of `Starlette`, along with the `RouterX` and `RouteX` classes it automatically uses. -- `def date(s)` +- `def parsed_date(s)` Convert `s` to a datetime - `def snake2hyphens(s)` @@ -2344,6 +2345,9 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def str2int(s)` Convert `s` to an `int` +- `def str2date(s)` + `date.fromisoformat` with empty string handling + - `@dataclass class HttpHeader` - `def __init__(self, k, v)` @@ -2621,12 +2625,12 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `@delegates(ft_hx, keep=True) def A(*c, **kwargs)` An A tag; `href` defaults to '#' for more concise use with HTMX -- `@delegates(ft_hx, keep=True) def Form(*c, **kwargs)` - A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'` - - `@delegates(ft_hx, keep=True) def AX(txt, hx_get, target_id, hx_swap, href, **kwargs)` An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params +- `@delegates(ft_hx, keep=True) def Form(*c, **kwargs)` + A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'` + - `@delegates(ft_hx, keep=True) def Hidden(value, id, **kwargs)` An Input of type 'hidden' @@ -2657,8 +2661,11 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def StyleX(fname, **kw)` A `style` element with contents read from `fname` and variables replaced from `kw` +- `def Surreal(code)` + Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')` + - `def On(code, event, sel, me)` - An async surreal.js script block event handler for `event` on selector `sel`, making available parent `p`, event `ev`, and target `e` + An async surreal.js script block event handler for `event` on selector `sel,p`, making available parent `p`, event `ev`, and target `e` - `def Prev(code, event)` An async surreal.js script block event handler for `event` on previous sibling, with same vars as `On` @@ -2672,6 +2679,9 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def run_js(js, id, **kw)` Run `js` script, auto-generating `id` based on name of caller if needed, and js-escaping any `kw` params +- `def jsd(org, repo, root, path, prov, typ, ver, esm, **kwargs)` + jsdelivr `Script` or CSS `Link` tag, or URL + - `@delegates(ft_hx, keep=True) def Titled(title, *args, **kwargs)` An HTML partial containing a `Title`, and `H1`, and any provided children @@ -2680,9 +2690,6 @@ class CustomHeaderMiddleware(BaseHTTPMiddleware): - `def Favicon(light_icon, dark_icon)` Light and dark favicon headers - -- `def jsd(org, repo, root, path, prov, typ, ver, esm, **kwargs)` - jsdelivr `Script` or CSS `Link` tag, or URL from asyncio import sleep from fasthtml.common import * diff --git a/nbs/tutorials/by_example.ipynb b/nbs/tutorials/by_example.ipynb index 1001deb4..19347781 100644 --- a/nbs/tutorials/by_example.ipynb +++ b/nbs/tutorials/by_example.ipynb @@ -1890,7 +1890,6 @@ "\n", "```javascript\n", "import { marked } from \"https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js\";\n", - "import { proc_htmx} from \"https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js/fasthtml.js\";\n", "proc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));\n", "```\n", "\n",