Skip to content

Commit

Permalink
fixes #533
Browse files Browse the repository at this point in the history
  • Loading branch information
jph00 committed Oct 20, 2024
1 parent 324ba37 commit 4048bd2
Show file tree
Hide file tree
Showing 17 changed files with 595 additions and 375 deletions.
2 changes: 1 addition & 1 deletion examples/basic_ws.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from asyncio import sleep
from fasthtml.common import *

app = FastHTML(ws_hdr=True)
app = FastHTML(exts='ws')
rt = app.route

def mk_inp(): return Input(id='msg')
Expand Down
23 changes: 10 additions & 13 deletions examples/chat_ws.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
from fasthtml.common import *

app = FastHTML(ws_hdr=True)
app = FastHTML(exts='ws')
rt = app.route

msgs = []
@rt('/')
def home(): return Div(
Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
Form(Input(id='msg'), id='form', ws_send=True),
hx_ext='ws', ws_connect='/ws')
def home():
return Div(hx_ext='ws', ws_connect='/ws')(
Div(Ul(*[Li(m) for m in msgs], id='msg-list')),
Form(Input(id='msg'), id='form', ws_send=True)
)

users = {}
def on_conn(ws, send): users[str(id(ws))] = send
def on_disconn(ws): users.pop(str(id(ws)), None)

@app.ws('/ws', conn=on_conn, disconn=on_disconn)
async def ws(msg:str):
msgs.append(msg)
# Use associated `send` function to send message to each user
for u in users.values(): await u(Ul(*[Li(m) for m in msgs], id='msg-list'))
await send(Ul(*[Li(m) for m in msgs], id='msg-list'))

send = setup_ws(app, ws)

serve()
serve()
8 changes: 5 additions & 3 deletions fasthtml/components.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ from fastcore.utils import *
from fastcore.xml import *
from fastcore.meta import use_kwargs, delegates
from fastcore.test import *
from .core import fh_cfg
from .core import fh_cfg, unqid
import types, json
try:
from IPython import display
Expand All @@ -23,15 +23,17 @@ hx_attrs = 'get post put delete patch trigger target swap swap_oob include selec
hx_attrs = [f'hx_{o}' for o in hx_attrs.split()]
hx_attrs_annotations = {'hx_swap': Literal['innerHTML', 'outerHTML', 'afterbegin', 'beforebegin', 'beforeend', 'afterend', 'delete', 'none'] | str, 'hx_swap_oob': Literal['true', 'innerHTML', 'outerHTML', 'afterbegin', 'beforebegin', 'beforeend', 'afterend', 'delete', 'none'] | str, 'hx_push_url': Literal['true', 'false'] | str, 'hx_replace_url': Literal['true', 'false'] | str, 'hx_disabled_elt': Literal['this', 'next', 'previous'] | str, 'hx_history': Literal['false'] | str, 'hx_params': Literal['*', 'none'] | str, 'hx_replace_url': Literal['true', 'false'] | str, 'hx_validate': Literal['true', 'false']}
hx_attrs_annotations |= {o: str for o in set(hx_attrs) - set(hx_attrs_annotations.keys())}
hx_attrs_annotations = {k: Optional[v] for (k, v) in hx_attrs_annotations.items()}
hx_attrs_annotations = {k: Optional[v] for k, v in hx_attrs_annotations.items()}
hx_attrs = html_attrs + hx_attrs

def attrmap_x(o):
...
fh_cfg['attrmap'] = attrmap_x
fh_cfg['valmap'] = valmap
fh_cfg['ft_cls'] = FT
fh_cfg['auto_id'] = False

def ft_html(tag: str, *c, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=FT, **kwargs):
def ft_html(tag: str, *c, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=None, auto_id=None, **kwargs):
...

@use_kwargs(hx_attrs, keep=True)
Expand Down
35 changes: 17 additions & 18 deletions fasthtml/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.

# %% auto 0
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'htmxwssrc', 'fhjsscr', 'htmxctsrc', 'surrsrc',
'scopesrc', 'viewport', 'charset', 'all_meths', 'parsed_date', 'snake2hyphens', 'HtmxHeaders', 'HttpHeader',
'HtmxResponseHeaders', 'form2dict', 'parse_form', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown',
'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', 'Redirect', 'RouteX', 'RouterX', 'get_key', 'def_hdrs',
'FastHTML', 'serve', 'Client', 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse', 'unqid', 'setup_ws']
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc', 'viewport',
'charset', 'all_meths', 'parsed_date', 'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders',
'form2dict', 'parse_form', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'WS_RouteX', 'uri',
'decode_uri', 'flat_tuple', 'Redirect', 'RouteX', 'RouterX', 'get_key', 'def_hdrs', 'FastHTML', 'serve',
'Client', 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse', 'unqid', 'setup_ws']

# %% ../nbs/api/00_core.ipynb
import json,uuid,inspect,types,uvicorn,signal,asyncio,threading
Expand Down Expand Up @@ -472,14 +472,14 @@ def add_ws(self, path: str, recv: callable, conn:callable=None, disconn:callable
"loading-states": "https://unpkg.com/[email protected]/loading-states.js",
"multi-swap": "https://unpkg.com/[email protected]/multi-swap.js",
"path-deps": "https://unpkg.com/[email protected]/path-deps.js",
"remove-me": "https://unpkg.com/[email protected]/remove-me.js"
"remove-me": "https://unpkg.com/[email protected]/remove-me.js",
"ws": "https://unpkg.com/htmx-ext-ws/ws.js",
"chunked-transfer": "https://unpkg.com/htmx-ext-transfer-encoding-chunked/transfer-encoding-chunked.js"
}

# %% ../nbs/api/00_core.ipynb
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/[email protected]/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")
viewport = Meta(name="viewport", content="width=device-width, initial-scale=1, viewport-fit=cover")
Expand Down Expand Up @@ -515,28 +515,27 @@ def __str__(self): return p
return _lf()

# %% ../nbs/api/00_core.ipynb
def def_hdrs(htmx=True, ct_hdr=False, ws_hdr=False, surreal=True):
def def_hdrs(htmx=True, surreal=True):
"Default headers for a FastHTML app"
hdrs = []
if surreal: hdrs = [surrsrc,scopesrc] + hdrs
if ws_hdr: hdrs = [htmxwssrc] + hdrs
if ct_hdr: hdrs = [htmxctsrc] + hdrs
if htmx: hdrs = [htmxsrc,fhjsscr] + hdrs
return [charset, viewport] + hdrs

# %% ../nbs/api/00_core.ipynb
class FastHTML(Starlette):
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,
before=None, after=None, ws_hdr=False, ct_hdr=False,
surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None,
before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
htmlkw=None, **bodykw):
middleware,before,after = map(_list, (middleware,before,after))
hdrs,ftrs = listify(hdrs),listify(ftrs)
hdrs,ftrs,exts = map(listify, (hdrs,ftrs,exts))
exts = {k:htmx_exts[k] for k in exts}
htmlkw = htmlkw or {}
if default_hdrs: hdrs = def_hdrs(htmx, ct_hdr=ct_hdr, ws_hdr=ws_hdr, surreal=surreal) + hdrs
if default_hdrs: hdrs = def_hdrs(htmx, surreal=surreal) + hdrs
hdrs += [Script(src=ext) for ext in exts.values()]
self.on_startup,self.on_shutdown,self.lifespan,self.hdrs,self.ftrs = on_startup,on_shutdown,lifespan,hdrs,ftrs
self.before,self.after,self.htmlkw,self.bodykw = before,after,htmlkw,bodykw
secret_key = get_key(secret_key, key_fname)
Expand Down Expand Up @@ -690,11 +689,11 @@ def _add_ids(s):
for c in s.children: _add_ids(c)

# %% ../nbs/api/00_core.ipynb
def setup_ws(app):
def setup_ws(app, f=noop):
conns = {}
async def on_connect(scope, send): conns[scope.client] = send
async def on_disconnect(scope): conns.pop(scope.client)
app.ws('/ws', conn=on_connect, disconn=on_disconnect)()
app.ws('/ws', conn=on_connect, disconn=on_disconnect)(f)
async def send(s):
for o in conns.values(): await o(s)
app._send = send
Expand Down
34 changes: 18 additions & 16 deletions fasthtml/core.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""The `FastHTML` subclass of `Starlette`, along with the `RouterX` and `RouteX` classes it automatically uses."""
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmxsrc', 'htmxwssrc', 'fhjsscr', 'htmxctsrc', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths', 'parsed_date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'str2date', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', 'Redirect', 'RouteX', 'RouterX', 'get_key', 'def_hdrs', 'FastHTML', 'serve', 'Client', 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse']
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths', 'parsed_date', 'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', 'Redirect', 'RouteX', 'RouterX', 'get_key', 'def_hdrs', 'FastHTML', 'serve', 'Client', 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse', 'unqid', 'setup_ws']
import json, uuid, inspect, types, uvicorn, signal, asyncio, threading
from fastcore.utils import *
from fastcore.xml import *
Expand All @@ -18,6 +18,8 @@ from warnings import warn
from dateutil import parser as dtparse
from httpx import ASGITransport, AsyncClient
from anyio import from_thread
from uuid import uuid4
from base64 import b85encode, b64encode
from .starlette import *
empty = Parameter.empty

Expand Down Expand Up @@ -50,19 +52,11 @@ class HtmxHeaders:
def _get_htmx(h):
...

def str2int(s) -> int:
"""Convert `s` to an `int`"""
...

def _mk_list(t, v):
...
fh_cfg = AttrDict(indent=True)

def str2date(s: str) -> date:
"""`date.fromisoformat` with empty string handling"""
...

def _fix_anno(t):
def _fix_anno(t, o):
"""Create appropriate callable type for casting a `str` to type `t` (or first type in `t` if union)"""
...

Expand Down Expand Up @@ -228,10 +222,9 @@ class RouterX(Router):

def add_ws(self, path: str, recv: callable, conn: callable=None, disconn: callable=None, name=None):
...
htmx_exts = {'head-support': 'https://unpkg.com/[email protected]/head-support.js', 'preload': 'https://unpkg.com/[email protected]/preload.js', 'class-tools': 'https://unpkg.com/[email protected]/class-tools.js', 'loading-states': 'https://unpkg.com/[email protected]/loading-states.js', 'multi-swap': 'https://unpkg.com/[email protected]/multi-swap.js', 'path-deps': 'https://unpkg.com/[email protected]/path-deps.js', 'remove-me': 'https://unpkg.com/[email protected]/remove-me.js', 'ws': 'https://unpkg.com/htmx-ext-ws/ws.js', 'chunked-transfer': 'https://unpkg.com/htmx-ext-transfer-encoding-chunked/transfer-encoding-chunked.js'}
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/[email protected]/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')
viewport = Meta(name='viewport', content='width=device-width, initial-scale=1, viewport-fit=cover')
Expand All @@ -249,13 +242,13 @@ def _wrap_ex(f, hdrs, ftrs, htmlkw, bodykw):
def _mk_locfunc(f, p):
...

def def_hdrs(htmx=True, ct_hdr=False, ws_hdr=False, surreal=True):
def def_hdrs(htmx=True, surreal=True):
"""Default headers for a FastHTML app"""
...

class FastHTML(Starlette):

def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None, on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, before=None, after=None, ws_hdr=False, ct_hdr=False, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware, secret_key=None, session_cookie='session_', max_age=365 * 24 * 3600, sess_path='/', same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', htmlkw=None, **bodykw):
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None, on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None, before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware, secret_key=None, session_cookie='session_', max_age=365 * 24 * 3600, sess_path='/', same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', htmlkw=None, **bodykw):
...

def ws(self, path: str, conn=None, disconn=None, name=None):
Expand Down Expand Up @@ -309,8 +302,17 @@ class MiddlewareBase:
class FtResponse:
"""Wrap an FT response with any Starlette `Response`"""

def __init__(self, content, status_code: int=200, headers=None, cls=HTMLResponse, media_type: str | None=None, background=None):
def __init__(self, content, status_code: int=200, headers=None, cls=HTMLResponse, media_type: str | None=None):
...

def __response__(self, req):
...
...

def unqid():
...

def _add_ids(s):
...

def setup_ws(app, f=noop):
...
4 changes: 2 additions & 2 deletions fasthtml/fastapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def fast_app(
pico:Optional[bool]=None, # Include PicoCSS header?
surreal:Optional[bool]=True, # Include surreal.js/scope headers?
htmx:Optional[bool]=True, # Include HTMX header?
ws_hdr:bool=False, # Include HTMX websocket extension header?
exts:Optional[list|str]=None, # HTMX extension names to include
secret_key:Optional[str]=None, # Signing key for sessions
key_fname:str='.sesskey', # Session cookie signing key file name
session_cookie:str='session_', # Session cookie name
Expand All @@ -76,7 +76,7 @@ def fast_app(
app = _app_factory(hdrs=h, ftrs=ftrs, before=before, middleware=middleware, live=live, debug=debug, routes=routes, exception_handlers=exception_handlers,
on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, default_hdrs=default_hdrs, secret_key=secret_key,
session_cookie=session_cookie, max_age=max_age, sess_path=sess_path, same_site=same_site, sess_https_only=sess_https_only,
sess_domain=sess_domain, key_fname=key_fname, ws_hdr=ws_hdr, surreal=surreal, htmx=htmx, htmlkw=htmlkw,
sess_domain=sess_domain, key_fname=key_fname, exts=exts, surreal=surreal, htmx=htmx, htmlkw=htmlkw,
reload_attempts=reload_attempts, reload_interval=reload_interval, **(bodykw or {}))
app.static_route_exts(static_path=static_path)
if not db_file: return app,app.route
Expand Down
12 changes: 8 additions & 4 deletions fasthtml/xtend.pyi
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Simple extensions to standard HTML components, such as adding sensible defaults"""
__all__ = ['sid_scr', 'A', 'AX', 'Form', 'Hidden', 'CheckboxX', 'Script', 'Style', 'double_braces', 'undouble_braces', 'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'Nbsp', 'Surreal', 'On', 'Prev', 'Now', 'AnyNow', 'run_js', 'HtmxOn', 'jsd', 'Titled', 'Socials', 'Favicon', 'clear']
__all__ = ['sid_scr', 'A', 'AX', 'Form', 'Hidden', 'CheckboxX', 'Script', 'Style', 'double_braces', 'undouble_braces', 'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'Nbsp', 'Surreal', 'On', 'Prev', 'Now', 'AnyNow', 'run_js', 'HtmxOn', 'jsd', 'Titled', 'Socials', 'Favicon', 'clear', 'with_sid']
from dataclasses import dataclass, asdict
from typing import Any
from fastcore.utils import *
from fastcore.xtras import partial_format
from fastcore.xml import *
from fastcore.meta import use_kwargs, delegates
from .core import *
from .components import *
try:
from IPython import display
Expand All @@ -32,11 +33,11 @@ def CheckboxX(checked: bool=False, label=None, value='1', id=None, name=None, *,
"""A Checkbox optionally inside a Label, preceded by a `Hidden` with matching name"""
...

def Script(code: str='', *, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=fastcore.xml.FT, **kwargs) -> FT:
def Script(code: str='', *, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=None, auto_id=None, **kwargs) -> FT:
"""A Script tag that doesn't escape its code"""
...

def Style(*c, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=fastcore.xml.FT, **kwargs) -> FT:
def Style(*c, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=None, auto_id=None, **kwargs) -> FT:
"""A Style tag that doesn't escape its code"""
...

Expand Down Expand Up @@ -113,4 +114,7 @@ def Favicon(light_icon, dark_icon):

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

def with_sid(app, dest, path='/'):
...
Loading

0 comments on commit 4048bd2

Please sign in to comment.