python-fasthtml 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python-fasthtml-0.1.0/python_fasthtml.egg-info → python_fasthtml-0.1.2}/PKG-INFO +2 -2
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/__init__.py +4 -2
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/_modidx.py +47 -40
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/common.py +0 -1
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/components.py +4 -1
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/components.pyi +1 -0
- python-fasthtml-0.1.0/fasthtml/test.py → python_fasthtml-0.1.2/fasthtml/core.py +135 -60
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/fastapp.py +2 -2
- python_fasthtml-0.1.2/fasthtml/toaster.py +58 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/xtend.py +13 -6
- python_fasthtml-0.1.2/fasthtml/xtend.pyi +77 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2/python_fasthtml.egg-info}/PKG-INFO +2 -2
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/SOURCES.txt +1 -1
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/requires.txt +1 -1
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/settings.ini +3 -3
- python-fasthtml-0.1.0/fasthtml/core.py +0 -278
- python-fasthtml-0.1.0/fasthtml/xtend.pyi +0 -45
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/LICENSE +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/MANIFEST.in +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/README.md +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/authmw.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/cli.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/js.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/live_reload.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/oauth.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/starlette.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/svg.py +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/not-zip-safe +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/setup.cfg +0 -0
- {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: The fastest way to create an HTML app
|
|
5
5
|
Home-page: https://github.com/AnswerDotAI/fasthtml
|
|
6
6
|
Author: Jeremy Howard
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: fastcore>=1.5.46
|
|
19
19
|
Requires-Dist: python-dateutil
|
|
20
|
-
Requires-Dist: starlette
|
|
20
|
+
Requires-Dist: starlette>0.33
|
|
21
21
|
Requires-Dist: oauthlib
|
|
22
22
|
Requires-Dist: itsdangerous
|
|
23
23
|
Requires-Dist: uvicorn[standard]
|
|
@@ -19,7 +19,51 @@ d = { 'settings': { 'branch': 'main',
|
|
|
19
19
|
'fasthtml.components.show': ('components.html#show', 'fasthtml/components.py'),
|
|
20
20
|
'fasthtml.components.xt_html': ('components.html#xt_html', 'fasthtml/components.py'),
|
|
21
21
|
'fasthtml.components.xt_hx': ('components.html#xt_hx', 'fasthtml/components.py')},
|
|
22
|
-
'fasthtml.core': {
|
|
22
|
+
'fasthtml.core': { 'fasthtml.core.Beforeware': ('core.html#beforeware', 'fasthtml/core.py'),
|
|
23
|
+
'fasthtml.core.Beforeware.__init__': ('core.html#beforeware.__init__', 'fasthtml/core.py'),
|
|
24
|
+
'fasthtml.core.FastHTML': ('core.html#fasthtml', 'fasthtml/core.py'),
|
|
25
|
+
'fasthtml.core.FastHTML.__init__': ('core.html#fasthtml.__init__', 'fasthtml/core.py'),
|
|
26
|
+
'fasthtml.core.FastHTML.route': ('core.html#fasthtml.route', 'fasthtml/core.py'),
|
|
27
|
+
'fasthtml.core.FastHTML.ws': ('core.html#fasthtml.ws', 'fasthtml/core.py'),
|
|
28
|
+
'fasthtml.core.HtmxHeaders': ('core.html#htmxheaders', 'fasthtml/core.py'),
|
|
29
|
+
'fasthtml.core.HtmxHeaders.__bool__': ('core.html#htmxheaders.__bool__', 'fasthtml/core.py'),
|
|
30
|
+
'fasthtml.core.HttpHeader': ('core.html#httpheader', 'fasthtml/core.py'),
|
|
31
|
+
'fasthtml.core.MiddlewareBase': ('core.html#middlewarebase', 'fasthtml/core.py'),
|
|
32
|
+
'fasthtml.core.MiddlewareBase.__call__': ('core.html#middlewarebase.__call__', 'fasthtml/core.py'),
|
|
33
|
+
'fasthtml.core.RouteX': ('core.html#routex', 'fasthtml/core.py'),
|
|
34
|
+
'fasthtml.core.RouteX.__init__': ('core.html#routex.__init__', 'fasthtml/core.py'),
|
|
35
|
+
'fasthtml.core.RouterX': ('core.html#routerx', 'fasthtml/core.py'),
|
|
36
|
+
'fasthtml.core.RouterX.__init__': ('core.html#routerx.__init__', 'fasthtml/core.py'),
|
|
37
|
+
'fasthtml.core.RouterX.add_route': ('core.html#routerx.add_route', 'fasthtml/core.py'),
|
|
38
|
+
'fasthtml.core.RouterX.add_ws': ('core.html#routerx.add_ws', 'fasthtml/core.py'),
|
|
39
|
+
'fasthtml.core.WS_RouteX': ('core.html#ws_routex', 'fasthtml/core.py'),
|
|
40
|
+
'fasthtml.core.WS_RouteX.__init__': ('core.html#ws_routex.__init__', 'fasthtml/core.py'),
|
|
41
|
+
'fasthtml.core._annotations': ('core.html#_annotations', 'fasthtml/core.py'),
|
|
42
|
+
'fasthtml.core._find_p': ('core.html#_find_p', 'fasthtml/core.py'),
|
|
43
|
+
'fasthtml.core._find_wsp': ('core.html#_find_wsp', 'fasthtml/core.py'),
|
|
44
|
+
'fasthtml.core._fix_anno': ('core.html#_fix_anno', 'fasthtml/core.py'),
|
|
45
|
+
'fasthtml.core._form_arg': ('core.html#_form_arg', 'fasthtml/core.py'),
|
|
46
|
+
'fasthtml.core._formitem': ('core.html#_formitem', 'fasthtml/core.py'),
|
|
47
|
+
'fasthtml.core._from_body': ('core.html#_from_body', 'fasthtml/core.py'),
|
|
48
|
+
'fasthtml.core._get_htmx': ('core.html#_get_htmx', 'fasthtml/core.py'),
|
|
49
|
+
'fasthtml.core._is_body': ('core.html#_is_body', 'fasthtml/core.py'),
|
|
50
|
+
'fasthtml.core._list': ('core.html#_list', 'fasthtml/core.py'),
|
|
51
|
+
'fasthtml.core._send_ws': ('core.html#_send_ws', 'fasthtml/core.py'),
|
|
52
|
+
'fasthtml.core._wrap_ep': ('core.html#_wrap_ep', 'fasthtml/core.py'),
|
|
53
|
+
'fasthtml.core._wrap_req': ('core.html#_wrap_req', 'fasthtml/core.py'),
|
|
54
|
+
'fasthtml.core._wrap_resp': ('core.html#_wrap_resp', 'fasthtml/core.py'),
|
|
55
|
+
'fasthtml.core._wrap_ws': ('core.html#_wrap_ws', 'fasthtml/core.py'),
|
|
56
|
+
'fasthtml.core._ws_endp': ('core.html#_ws_endp', 'fasthtml/core.py'),
|
|
57
|
+
'fasthtml.core._xt_resp': ('core.html#_xt_resp', 'fasthtml/core.py'),
|
|
58
|
+
'fasthtml.core.date': ('core.html#date', 'fasthtml/core.py'),
|
|
59
|
+
'fasthtml.core.flat_xt': ('core.html#flat_xt', 'fasthtml/core.py'),
|
|
60
|
+
'fasthtml.core.form2dict': ('core.html#form2dict', 'fasthtml/core.py'),
|
|
61
|
+
'fasthtml.core.get_key': ('core.html#get_key', 'fasthtml/core.py'),
|
|
62
|
+
'fasthtml.core.is_namedtuple': ('core.html#is_namedtuple', 'fasthtml/core.py'),
|
|
63
|
+
'fasthtml.core.is_typeddict': ('core.html#is_typeddict', 'fasthtml/core.py'),
|
|
64
|
+
'fasthtml.core.reg_re_param': ('core.html#reg_re_param', 'fasthtml/core.py'),
|
|
65
|
+
'fasthtml.core.snake2hyphens': ('core.html#snake2hyphens', 'fasthtml/core.py'),
|
|
66
|
+
'fasthtml.core.str2int': ('core.html#str2int', 'fasthtml/core.py')},
|
|
23
67
|
'fasthtml.fastapp': {},
|
|
24
68
|
'fasthtml.js': {},
|
|
25
69
|
'fasthtml.live_reload': {},
|
|
@@ -38,45 +82,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
38
82
|
'fasthtml.oauth.retr_code': ('oauth.html#retr_code', 'fasthtml/oauth.py')},
|
|
39
83
|
'fasthtml.starlette': {},
|
|
40
84
|
'fasthtml.svg': {},
|
|
41
|
-
'fasthtml.
|
|
42
|
-
'fasthtml.test.Beforeware.__init__': ('core_tests.html#beforeware.__init__', 'fasthtml/test.py'),
|
|
43
|
-
'fasthtml.test.FastHTML': ('core_tests.html#fasthtml', 'fasthtml/test.py'),
|
|
44
|
-
'fasthtml.test.FastHTML.__init__': ('core_tests.html#fasthtml.__init__', 'fasthtml/test.py'),
|
|
45
|
-
'fasthtml.test.FastHTML.route': ('core_tests.html#fasthtml.route', 'fasthtml/test.py'),
|
|
46
|
-
'fasthtml.test.HtmxHeaders': ('core_tests.html#htmxheaders', 'fasthtml/test.py'),
|
|
47
|
-
'fasthtml.test.HtmxHeaders.__bool__': ('core_tests.html#htmxheaders.__bool__', 'fasthtml/test.py'),
|
|
48
|
-
'fasthtml.test.HttpHeader': ('core_tests.html#httpheader', 'fasthtml/test.py'),
|
|
49
|
-
'fasthtml.test.MiddlewareBase': ('core_tests.html#middlewarebase', 'fasthtml/test.py'),
|
|
50
|
-
'fasthtml.test.MiddlewareBase.__call__': ('core_tests.html#middlewarebase.__call__', 'fasthtml/test.py'),
|
|
51
|
-
'fasthtml.test.RouteX': ('core_tests.html#routex', 'fasthtml/test.py'),
|
|
52
|
-
'fasthtml.test.RouteX.__init__': ('core_tests.html#routex.__init__', 'fasthtml/test.py'),
|
|
53
|
-
'fasthtml.test.RouterX': ('core_tests.html#routerx', 'fasthtml/test.py'),
|
|
54
|
-
'fasthtml.test.RouterX.__init__': ('core_tests.html#routerx.__init__', 'fasthtml/test.py'),
|
|
55
|
-
'fasthtml.test.RouterX.add_route': ('core_tests.html#routerx.add_route', 'fasthtml/test.py'),
|
|
56
|
-
'fasthtml.test.RouterX.add_ws': ('core_tests.html#routerx.add_ws', 'fasthtml/test.py'),
|
|
57
|
-
'fasthtml.test.WS_RouteX': ('core_tests.html#ws_routex', 'fasthtml/test.py'),
|
|
58
|
-
'fasthtml.test.WS_RouteX.__init__': ('core_tests.html#ws_routex.__init__', 'fasthtml/test.py'),
|
|
59
|
-
'fasthtml.test._annotations': ('core_tests.html#_annotations', 'fasthtml/test.py'),
|
|
60
|
-
'fasthtml.test._find_p': ('core_tests.html#_find_p', 'fasthtml/test.py'),
|
|
61
|
-
'fasthtml.test._fix_anno': ('core_tests.html#_fix_anno', 'fasthtml/test.py'),
|
|
62
|
-
'fasthtml.test._form_arg': ('core_tests.html#_form_arg', 'fasthtml/test.py'),
|
|
63
|
-
'fasthtml.test._formitem': ('core_tests.html#_formitem', 'fasthtml/test.py'),
|
|
64
|
-
'fasthtml.test._from_body': ('core_tests.html#_from_body', 'fasthtml/test.py'),
|
|
65
|
-
'fasthtml.test._get_htmx': ('core_tests.html#_get_htmx', 'fasthtml/test.py'),
|
|
66
|
-
'fasthtml.test._is_body': ('core_tests.html#_is_body', 'fasthtml/test.py'),
|
|
67
|
-
'fasthtml.test._wrap_ep': ('core_tests.html#_wrap_ep', 'fasthtml/test.py'),
|
|
68
|
-
'fasthtml.test._wrap_req': ('core_tests.html#_wrap_req', 'fasthtml/test.py'),
|
|
69
|
-
'fasthtml.test._wrap_resp': ('core_tests.html#_wrap_resp', 'fasthtml/test.py'),
|
|
70
|
-
'fasthtml.test._xt_resp': ('core_tests.html#_xt_resp', 'fasthtml/test.py'),
|
|
71
|
-
'fasthtml.test.date': ('core_tests.html#date', 'fasthtml/test.py'),
|
|
72
|
-
'fasthtml.test.flat_xt': ('core_tests.html#flat_xt', 'fasthtml/test.py'),
|
|
73
|
-
'fasthtml.test.form2dict': ('core_tests.html#form2dict', 'fasthtml/test.py'),
|
|
74
|
-
'fasthtml.test.get_key': ('core_tests.html#get_key', 'fasthtml/test.py'),
|
|
75
|
-
'fasthtml.test.is_namedtuple': ('core_tests.html#is_namedtuple', 'fasthtml/test.py'),
|
|
76
|
-
'fasthtml.test.is_typeddict': ('core_tests.html#is_typeddict', 'fasthtml/test.py'),
|
|
77
|
-
'fasthtml.test.reg_re_param': ('core_tests.html#reg_re_param', 'fasthtml/test.py'),
|
|
78
|
-
'fasthtml.test.snake2hyphens': ('core_tests.html#snake2hyphens', 'fasthtml/test.py'),
|
|
79
|
-
'fasthtml.test.str2int': ('core_tests.html#str2int', 'fasthtml/test.py')},
|
|
85
|
+
'fasthtml.toaster': {},
|
|
80
86
|
'fasthtml.xtend': { 'fasthtml.xtend.A': ('xtend.html#a', 'fasthtml/xtend.py'),
|
|
81
87
|
'fasthtml.xtend.AX': ('xtend.html#ax', 'fasthtml/xtend.py'),
|
|
82
88
|
'fasthtml.xtend.Card': ('xtend.html#card', 'fasthtml/xtend.py'),
|
|
@@ -92,4 +98,5 @@ d = { 'settings': { 'branch': 'main',
|
|
|
92
98
|
'fasthtml.xtend.Style': ('xtend.html#style', 'fasthtml/xtend.py'),
|
|
93
99
|
'fasthtml.xtend.Titled': ('xtend.html#titled', 'fasthtml/xtend.py'),
|
|
94
100
|
'fasthtml.xtend.jsd': ('xtend.html#jsd', 'fasthtml/xtend.py'),
|
|
101
|
+
'fasthtml.xtend.run_js': ('xtend.html#run_js', 'fasthtml/xtend.py'),
|
|
95
102
|
'fasthtml.xtend.set_pico_cls': ('xtend.html#set_pico_cls', 'fasthtml/xtend.py')}}}
|
|
@@ -15,13 +15,14 @@ __all__ = ['voids', 'named', 'html_attrs', 'hx_attrs', 'show', 'xt_html', 'xt_hx
|
|
|
15
15
|
|
|
16
16
|
# %% ../nbs/01_components.ipynb 2
|
|
17
17
|
from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING
|
|
18
|
-
|
|
19
18
|
from bs4 import BeautifulSoup
|
|
20
19
|
|
|
21
20
|
from fastcore.utils import *
|
|
22
21
|
from fastcore.xml import *
|
|
23
22
|
from fastcore.meta import use_kwargs, delegates
|
|
24
23
|
|
|
24
|
+
import types
|
|
25
|
+
|
|
25
26
|
try: from IPython import display
|
|
26
27
|
except ImportError: display=None
|
|
27
28
|
|
|
@@ -39,6 +40,7 @@ hx_attrs = html_attrs + [f'hx_{o}' for o in hx_attrs.split()]
|
|
|
39
40
|
|
|
40
41
|
# %% ../nbs/01_components.ipynb 6
|
|
41
42
|
def xt_html(tag: str, *c, id=None, cls=None, title=None, style=None, **kwargs):
|
|
43
|
+
if len(c)==1 and isinstance(c[0], (types.GeneratorType, map, filter)): c = tuple(c[0])
|
|
42
44
|
kwargs['id'],kwargs['cls'],kwargs['title'],kwargs['style'] = id,cls,title,style
|
|
43
45
|
tag,c,kw = xt(tag, *c, **kwargs)
|
|
44
46
|
if tag in named and 'id' in kw and 'name' not in kw: kw['name'] = kw['id']
|
|
@@ -108,6 +110,7 @@ def find_inputs(e, tags='input', **kw):
|
|
|
108
110
|
# %% ../nbs/01_components.ipynb 21
|
|
109
111
|
def __getattr__(tag):
|
|
110
112
|
if tag.startswith('_') or tag[0].islower(): raise AttributeError
|
|
113
|
+
tag = tag.replace("_", "-")
|
|
111
114
|
def _f(*c, target_id=None, **kwargs): return xt_hx(tag, *c, target_id=target_id, **kwargs)
|
|
112
115
|
return _f
|
|
113
116
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
def xt_html(tag: str, *c, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, hx_get:str|None=None, hx_post:str|None=None, hx_put:str|None=None, hx_delete:str|None=None, hx_patch:str|None=None, hx_trigger:str|None=None, hx_target:str|None=None, hx_swap:str|None=None, hx_include:str|None=None, hx_select:str|None=None, hx_indicator:str|None=None, hx_push_url:str|None=None, hx_confirm:str|None=None, hx_disable:str|None=None, hx_replace_url:str|None=None, hx_on:str|None=None, **kwargs): ...
|
|
1
2
|
def xt_hx(tag: str, *c, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, hx_get:str|None=None, hx_post:str|None=None, hx_put:str|None=None, hx_delete:str|None=None, hx_patch:str|None=None, hx_trigger:str|None=None, hx_target:str|None=None, hx_swap:str|None=None, hx_include:str|None=None, hx_select:str|None=None, hx_indicator:str|None=None, hx_push_url:str|None=None, hx_confirm:str|None=None, hx_disable:str|None=None, hx_replace_url:str|None=None, hx_on:str|None=None, **kwargs): ...
|
|
2
3
|
def A(*c, name:str|None=None, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, hx_get:str|None=None, hx_post:str|None=None, hx_put:str|None=None, hx_delete:str|None=None, hx_patch:str|None=None, hx_trigger:str|None=None, hx_target:str|None=None, hx_swap:str|None=None, hx_include:str|None=None, hx_select:str|None=None, hx_indicator:str|None=None, hx_push_url:str|None=None, hx_confirm:str|None=None, hx_disable:str|None=None, hx_replace_url:str|None=None, hx_on:str|None=None, **kwargs): ...
|
|
3
4
|
def Abbr(*c, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, id:str|None=None, cls:str|None=None, title:str|None=None, style:str|None=None, accesskey:str|None=None, contenteditable:str|None=None, dir:str|None=None, draggable:str|None=None, enterkeyhint:str|None=None, hidden:str|None=None, inert:str|None=None, inputmode:str|None=None, lang:str|None=None, popover:str|None=None, spellcheck:str|None=None, tabindex:str|None=None, translate:str|None=None, hx_get:str|None=None, hx_post:str|None=None, hx_put:str|None=None, hx_delete:str|None=None, hx_patch:str|None=None, hx_trigger:str|None=None, hx_target:str|None=None, hx_swap:str|None=None, hx_include:str|None=None, hx_select:str|None=None, hx_indicator:str|None=None, hx_push_url:str|None=None, hx_confirm:str|None=None, hx_disable:str|None=None, hx_replace_url:str|None=None, hx_on:str|None=None, **kwargs): ...
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/
|
|
1
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
|
|
2
2
|
|
|
3
3
|
# %% auto 0
|
|
4
|
-
__all__ = ['empty', 'htmx_hdrs', 'htmxscr', 'htmxwsscr', 'surrsrc', 'scopesrc', '
|
|
5
|
-
'
|
|
6
|
-
'
|
|
4
|
+
__all__ = ['empty', 'htmx_hdrs', 'htmxscr', 'htmxwsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths',
|
|
5
|
+
'is_typeddict', 'is_namedtuple', 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader',
|
|
6
|
+
'form2dict', 'flat_xt', 'Beforeware', 'WS_RouteX', 'RouteX', 'RouterX', 'get_key', 'FastHTML',
|
|
7
|
+
'reg_re_param', 'MiddlewareBase']
|
|
7
8
|
|
|
8
|
-
# %% ../nbs/
|
|
9
|
-
import json,dateutil,uuid,inspect
|
|
9
|
+
# %% ../nbs/00_core.ipynb 3
|
|
10
|
+
import json,dateutil,uuid,inspect,types
|
|
10
11
|
|
|
11
12
|
from fastcore.utils import *
|
|
12
13
|
from fastcore.xml import *
|
|
@@ -19,34 +20,35 @@ from dataclasses import dataclass,fields,is_dataclass,MISSING,asdict
|
|
|
19
20
|
from collections import namedtuple
|
|
20
21
|
from inspect import isfunction,ismethod,signature,Parameter,get_annotations
|
|
21
22
|
from functools import wraps, partialmethod
|
|
23
|
+
from copy import deepcopy
|
|
22
24
|
|
|
23
25
|
from .starlette import *
|
|
24
26
|
|
|
25
27
|
empty = Parameter.empty
|
|
26
28
|
|
|
27
|
-
# %% ../nbs/
|
|
29
|
+
# %% ../nbs/00_core.ipynb 5
|
|
28
30
|
def is_typeddict(cls:type)->bool:
|
|
29
31
|
"Check if `cls` is a `TypedDict`"
|
|
30
32
|
attrs = 'annotations', 'required_keys', 'optional_keys'
|
|
31
33
|
return isinstance(cls, type) and all(hasattr(cls, f'__{attr}__') for attr in attrs)
|
|
32
34
|
|
|
33
|
-
# %% ../nbs/
|
|
35
|
+
# %% ../nbs/00_core.ipynb 7
|
|
34
36
|
def is_namedtuple(cls):
|
|
35
37
|
"`True` is `cls` is a namedtuple type"
|
|
36
38
|
return issubclass(cls, tuple) and hasattr(cls, '_fields')
|
|
37
39
|
|
|
38
|
-
# %% ../nbs/
|
|
40
|
+
# %% ../nbs/00_core.ipynb 9
|
|
39
41
|
def date(s:str):
|
|
40
42
|
"Convert `s` to a datetime"
|
|
41
43
|
return dateutil.parser.parse(s)
|
|
42
44
|
|
|
43
|
-
# %% ../nbs/
|
|
45
|
+
# %% ../nbs/00_core.ipynb 11
|
|
44
46
|
def snake2hyphens(s:str):
|
|
45
47
|
"Convert `s` from snake case to hyphenated and capitalised"
|
|
46
48
|
s = snake2camel(s)
|
|
47
49
|
return camel2words(s, '-')
|
|
48
50
|
|
|
49
|
-
# %% ../nbs/
|
|
51
|
+
# %% ../nbs/00_core.ipynb 13
|
|
50
52
|
htmx_hdrs = dict(
|
|
51
53
|
boosted="HX-Boosted",
|
|
52
54
|
current_url="HX-Current-URL",
|
|
@@ -63,11 +65,11 @@ class HtmxHeaders:
|
|
|
63
65
|
request:str|None=None; target:str|None=None; trigger_name:str|None=None; trigger:str|None=None
|
|
64
66
|
def __bool__(self): return any(hasattr(self,o) for o in htmx_hdrs)
|
|
65
67
|
|
|
66
|
-
def _get_htmx(
|
|
67
|
-
res = {k:
|
|
68
|
+
def _get_htmx(h):
|
|
69
|
+
res = {k:h.get(v.lower(), None) for k,v in htmx_hdrs.items()}
|
|
68
70
|
return HtmxHeaders(**res)
|
|
69
71
|
|
|
70
|
-
# %% ../nbs/
|
|
72
|
+
# %% ../nbs/00_core.ipynb 16
|
|
71
73
|
def str2int(s)->int:
|
|
72
74
|
"Convert `s` to an `int`"
|
|
73
75
|
s = s.lower()
|
|
@@ -75,7 +77,7 @@ def str2int(s)->int:
|
|
|
75
77
|
if s=='none': return 0
|
|
76
78
|
return 0 if not s else int(s)
|
|
77
79
|
|
|
78
|
-
# %% ../nbs/
|
|
80
|
+
# %% ../nbs/00_core.ipynb 20
|
|
79
81
|
def _fix_anno(t):
|
|
80
82
|
"Create appropriate callable type for casting a `str` to type `t` (or first type in `t` if union)"
|
|
81
83
|
origin = get_origin(t)
|
|
@@ -86,7 +88,7 @@ def _fix_anno(t):
|
|
|
86
88
|
if origin in (list,List): res = partial(_mk_list, res)
|
|
87
89
|
return res
|
|
88
90
|
|
|
89
|
-
# %% ../nbs/
|
|
91
|
+
# %% ../nbs/00_core.ipynb 25
|
|
90
92
|
def _form_arg(k, v, d):
|
|
91
93
|
"Get type by accessing key `k` from `d`, and use to cast `v`"
|
|
92
94
|
if v is None: return
|
|
@@ -95,31 +97,31 @@ def _form_arg(k, v, d):
|
|
|
95
97
|
if not anno: return v
|
|
96
98
|
return _fix_anno(anno)(v)
|
|
97
99
|
|
|
98
|
-
# %% ../nbs/
|
|
100
|
+
# %% ../nbs/00_core.ipynb 29
|
|
99
101
|
@dataclass
|
|
100
102
|
class HttpHeader: k:str;v:str
|
|
101
103
|
|
|
102
|
-
# %% ../nbs/
|
|
104
|
+
# %% ../nbs/00_core.ipynb 30
|
|
103
105
|
def _annotations(anno):
|
|
104
106
|
"Same as `get_annotations`, but also works on namedtuples"
|
|
105
107
|
if is_namedtuple(anno): return {o:str for o in anno._fields}
|
|
106
108
|
return get_annotations(anno)
|
|
107
109
|
|
|
108
|
-
# %% ../nbs/
|
|
110
|
+
# %% ../nbs/00_core.ipynb 31
|
|
109
111
|
def _is_body(anno): return issubclass(anno, (dict,ns)) or _annotations(anno)
|
|
110
112
|
|
|
111
|
-
# %% ../nbs/
|
|
113
|
+
# %% ../nbs/00_core.ipynb 32
|
|
112
114
|
def _formitem(form, k):
|
|
113
115
|
"Return single item `k` from `form` if len 1, otherwise return list"
|
|
114
116
|
o = form.getlist(k)
|
|
115
117
|
return o[0] if len(o) == 1 else o if o else None
|
|
116
118
|
|
|
117
|
-
# %% ../nbs/
|
|
119
|
+
# %% ../nbs/00_core.ipynb 33
|
|
118
120
|
def form2dict(form: FormData) -> dict:
|
|
119
121
|
"Convert starlette form data to a dict"
|
|
120
122
|
return {k: _formitem(form, k) for k in form}
|
|
121
123
|
|
|
122
|
-
# %% ../nbs/
|
|
124
|
+
# %% ../nbs/00_core.ipynb 35
|
|
123
125
|
async def _from_body(req, p):
|
|
124
126
|
form = await req.form()
|
|
125
127
|
anno = p.annotation
|
|
@@ -128,14 +130,14 @@ async def _from_body(req, p):
|
|
|
128
130
|
cargs = {k:_form_arg(k, v, d) for k,v in form2dict(form).items()}
|
|
129
131
|
return anno(**cargs)
|
|
130
132
|
|
|
131
|
-
# %% ../nbs/
|
|
133
|
+
# %% ../nbs/00_core.ipynb 37
|
|
132
134
|
async def _find_p(req, arg:str, p:Parameter):
|
|
133
135
|
"In `req` find param named `arg` of type in `p` (`arg` is ignored for body types)"
|
|
134
136
|
anno = p.annotation
|
|
135
137
|
# If there's an annotation of special types, return object of that type
|
|
136
138
|
if isinstance(anno, type):
|
|
137
139
|
if issubclass(anno, Request): return req
|
|
138
|
-
if issubclass(anno, HtmxHeaders): return _get_htmx(req)
|
|
140
|
+
if issubclass(anno, HtmxHeaders): return _get_htmx(req.headers)
|
|
139
141
|
if issubclass(anno, Starlette): return req.scope['app']
|
|
140
142
|
if _is_body(anno): return await _from_body(req, p)
|
|
141
143
|
# If there's no annotation, check for special names
|
|
@@ -143,7 +145,7 @@ async def _find_p(req, arg:str, p:Parameter):
|
|
|
143
145
|
if 'request'.startswith(arg.lower()): return req
|
|
144
146
|
if 'session'.startswith(arg.lower()): return req.scope.get('session', {})
|
|
145
147
|
if arg.lower()=='auth': return req.scope.get('auth', None)
|
|
146
|
-
if arg.lower()=='htmx': return _get_htmx(req)
|
|
148
|
+
if arg.lower()=='htmx': return _get_htmx(req.headers)
|
|
147
149
|
if arg.lower()=='app': return req.scope['app']
|
|
148
150
|
return None
|
|
149
151
|
# Look through path, cookies, headers, session, query, and body in that order
|
|
@@ -165,7 +167,7 @@ async def _find_p(req, arg:str, p:Parameter):
|
|
|
165
167
|
async def _wrap_req(req, params):
|
|
166
168
|
return [await _find_p(req, arg, p) for arg,p in params.items()]
|
|
167
169
|
|
|
168
|
-
# %% ../nbs/
|
|
170
|
+
# %% ../nbs/00_core.ipynb 39
|
|
169
171
|
def flat_xt(lst):
|
|
170
172
|
"Flatten lists, except for `XT`s"
|
|
171
173
|
result = []
|
|
@@ -174,19 +176,21 @@ def flat_xt(lst):
|
|
|
174
176
|
else: result.append(item)
|
|
175
177
|
return result
|
|
176
178
|
|
|
177
|
-
# %% ../nbs/
|
|
179
|
+
# %% ../nbs/00_core.ipynb 41
|
|
178
180
|
def _xt_resp(req, resp, hdrs, **bodykw):
|
|
179
|
-
if not isinstance(resp,
|
|
181
|
+
if not isinstance(resp, tuple): resp = (resp,)
|
|
182
|
+
resp = resp + tuple(req.injects)
|
|
180
183
|
http_hdrs,resp = partition(resp, risinstance(HttpHeader))
|
|
181
184
|
http_hdrs = {o.k:str(o.v) for o in http_hdrs}
|
|
182
|
-
titles,bdy = partition(resp, lambda o: getattr(o, 'tag', '')
|
|
185
|
+
titles,bdy = partition(resp, lambda o: getattr(o, 'tag', '') in ('title','meta'))
|
|
183
186
|
if resp and 'hx-request' not in req.headers and not any(getattr(o, 'tag', '')=='html' for o in resp):
|
|
184
187
|
if not titles: titles = [Title('FastHTML page')]
|
|
185
|
-
resp = Html(Head(titles
|
|
188
|
+
resp = Html(Head(*titles, *flat_xt(hdrs)), Body(bdy, **bodykw))
|
|
186
189
|
return HTMLResponse(to_xml(resp), headers=http_hdrs)
|
|
187
190
|
|
|
188
|
-
# %% ../nbs/
|
|
191
|
+
# %% ../nbs/00_core.ipynb 42
|
|
189
192
|
def _wrap_resp(req, resp, cls, hdrs, **bodykw):
|
|
193
|
+
if not resp: resp=()
|
|
190
194
|
if isinstance(resp, FileResponse) and not os.path.exists(resp.path): raise HTTPException(404, resp.path)
|
|
191
195
|
if isinstance(resp, Response): return resp
|
|
192
196
|
if cls is not empty: return cls(resp)
|
|
@@ -198,12 +202,12 @@ def _wrap_resp(req, resp, cls, hdrs, **bodykw):
|
|
|
198
202
|
cls = HTMLResponse
|
|
199
203
|
return cls(resp)
|
|
200
204
|
|
|
201
|
-
# %% ../nbs/
|
|
205
|
+
# %% ../nbs/00_core.ipynb 43
|
|
202
206
|
class Beforeware:
|
|
203
207
|
def __init__(self, f, skip=None): self.f,self.skip = f,skip or []
|
|
204
208
|
|
|
205
|
-
# %% ../nbs/
|
|
206
|
-
def _wrap_ep(f, hdrs, before, **bodykw):
|
|
209
|
+
# %% ../nbs/00_core.ipynb 44
|
|
210
|
+
def _wrap_ep(f, hdrs, before, after, **bodykw):
|
|
207
211
|
if not (isfunction(f) or ismethod(f)): return f
|
|
208
212
|
sig = signature(f)
|
|
209
213
|
params = sig.parameters
|
|
@@ -211,6 +215,7 @@ def _wrap_ep(f, hdrs, before, **bodykw):
|
|
|
211
215
|
|
|
212
216
|
async def _f(req):
|
|
213
217
|
resp = None
|
|
218
|
+
req.injects = []
|
|
214
219
|
for b in before:
|
|
215
220
|
if not resp:
|
|
216
221
|
if isinstance(b, Beforeware): bf,skip = b.f,b.skip
|
|
@@ -223,45 +228,105 @@ def _wrap_ep(f, hdrs, before, **bodykw):
|
|
|
223
228
|
wreq = await _wrap_req(req, params)
|
|
224
229
|
resp = f(*wreq)
|
|
225
230
|
if is_async_callable(f): resp = await resp
|
|
231
|
+
for a in after:
|
|
232
|
+
_,*wreq = await _wrap_req(req, signature(a).parameters)
|
|
233
|
+
nr = a(resp, *wreq)
|
|
234
|
+
if nr: resp = nr
|
|
226
235
|
return _wrap_resp(req, resp, cls, hdrs, **bodykw)
|
|
227
236
|
return _f
|
|
228
237
|
|
|
229
|
-
# %% ../nbs/
|
|
238
|
+
# %% ../nbs/00_core.ipynb 46
|
|
239
|
+
def _find_wsp(ws, data, hdrs, arg:str, p:Parameter):
|
|
240
|
+
"In `data` find param named `arg` of type in `p` (`arg` is ignored for body types)"
|
|
241
|
+
anno = p.annotation
|
|
242
|
+
if isinstance(anno, type):
|
|
243
|
+
if issubclass(anno, HtmxHeaders): return _get_htmx(hdrs)
|
|
244
|
+
if issubclass(anno, Starlette): return ws.scope['app']
|
|
245
|
+
if anno is empty:
|
|
246
|
+
if arg.lower()=='ws': return ws
|
|
247
|
+
if arg.lower()=='data': return data
|
|
248
|
+
if arg.lower()=='htmx': return _get_htmx(hdrs)
|
|
249
|
+
if arg.lower()=='app': return ws.scope['app']
|
|
250
|
+
if arg.lower()=='send': return partial(_send_ws, ws)
|
|
251
|
+
return None
|
|
252
|
+
res = data.get(arg, None)
|
|
253
|
+
if res is empty or res is None: res = headers.get(snake2hyphens(arg), None)
|
|
254
|
+
if res is empty or res is None: res = p.default
|
|
255
|
+
# We can cast str and list[str] to types; otherwise just return what we have
|
|
256
|
+
if not isinstance(res, (list,str)) or anno is empty: return res
|
|
257
|
+
anno = _fix_anno(anno)
|
|
258
|
+
return [anno(o) for o in res] if isinstance(res,list) else anno(res)
|
|
259
|
+
|
|
260
|
+
def _wrap_ws(ws, data, params):
|
|
261
|
+
hdrs = data.pop('HEADERS', {})
|
|
262
|
+
return [_find_wsp(ws, data, hdrs, arg, p) for arg,p in params.items()]
|
|
263
|
+
|
|
264
|
+
# %% ../nbs/00_core.ipynb 47
|
|
265
|
+
async def _send_ws(ws, resp):
|
|
266
|
+
if not resp: return
|
|
267
|
+
res = to_xml(resp) if isinstance(resp, (list,tuple)) or hasattr(resp, '__xt__') else resp
|
|
268
|
+
await ws.send_text(res)
|
|
269
|
+
|
|
270
|
+
def _ws_endp(recv, conn=None, disconn=None, hdrs=None, before=None, **bodykw):
|
|
271
|
+
cls = type('WS_Endp', (WebSocketEndpoint,), {"encoding":"text"})
|
|
272
|
+
|
|
273
|
+
async def _generic_handler(handler, ws, data=None):
|
|
274
|
+
wd = _wrap_ws(ws, loads(data) if data else {}, signature(handler).parameters)
|
|
275
|
+
resp = handler(*wd)
|
|
276
|
+
if resp:
|
|
277
|
+
if is_async_callable(handler): resp = await resp
|
|
278
|
+
await _send_ws(ws, resp)
|
|
279
|
+
|
|
280
|
+
async def _connect(self, ws):
|
|
281
|
+
await ws.accept()
|
|
282
|
+
await _generic_handler(conn, ws)
|
|
283
|
+
async def _disconnect(self, ws, close_code): await _generic_handler(disconn, ws)
|
|
284
|
+
async def _recv(self, ws, data): await _generic_handler(recv, ws, data)
|
|
285
|
+
|
|
286
|
+
if conn: cls.on_connect = _connect
|
|
287
|
+
if disconn: cls.on_disconnect = _disconnect
|
|
288
|
+
cls.on_receive = _recv
|
|
289
|
+
return cls
|
|
290
|
+
|
|
291
|
+
# %% ../nbs/00_core.ipynb 50
|
|
230
292
|
class WS_RouteX(WebSocketRoute):
|
|
231
|
-
def __init__(self, path:str,
|
|
232
|
-
|
|
293
|
+
def __init__(self, path:str, recv, conn:callable=None, disconn:callable=None, *,
|
|
294
|
+
name=None, middleware=None, hdrs=None, before=None, **bodykw):
|
|
295
|
+
super().__init__(path, _ws_endp(recv, conn, disconn, hdrs, before, **bodykw), name=name, middleware=middleware)
|
|
233
296
|
|
|
234
|
-
# %% ../nbs/
|
|
297
|
+
# %% ../nbs/00_core.ipynb 51
|
|
235
298
|
class RouteX(Route):
|
|
236
299
|
def __init__(self, path:str, endpoint, *, methods=None, name=None, include_in_schema=True, middleware=None,
|
|
237
|
-
hdrs=None, before=None, **bodykw):
|
|
238
|
-
|
|
239
|
-
|
|
300
|
+
hdrs=None, before=None, after=None, **bodykw):
|
|
301
|
+
ep = _wrap_ep(endpoint, hdrs, before=before, after=after, **bodykw)
|
|
302
|
+
super().__init__(path, ep, methods=methods, name=name, include_in_schema=include_in_schema, middleware=middleware)
|
|
240
303
|
|
|
241
|
-
# %% ../nbs/
|
|
304
|
+
# %% ../nbs/00_core.ipynb 52
|
|
242
305
|
class RouterX(Router):
|
|
243
306
|
def __init__(self, routes=None, redirect_slashes=True, default=None, on_startup=None, on_shutdown=None,
|
|
244
|
-
lifespan=None, *, middleware=None, hdrs=None, before=None, **bodykw):
|
|
307
|
+
lifespan=None, *, middleware=None, hdrs=None, before=None, after=None, **bodykw):
|
|
245
308
|
super().__init__(routes, redirect_slashes, default, on_startup, on_shutdown,
|
|
246
309
|
lifespan=lifespan, middleware=middleware)
|
|
247
|
-
self.hdrs,self.bodykw,self.before = hdrs
|
|
310
|
+
self.hdrs,self.bodykw,self.before,self.after = hdrs,bodykw,before,after
|
|
248
311
|
|
|
249
312
|
def add_route( self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True):
|
|
250
313
|
route = RouteX(path, endpoint=endpoint, methods=methods, name=name, include_in_schema=include_in_schema,
|
|
251
|
-
hdrs=self.hdrs, before=self.before, **self.bodykw)
|
|
314
|
+
hdrs=self.hdrs, before=self.before, after=self.after, **self.bodykw)
|
|
252
315
|
self.routes.append(route)
|
|
253
316
|
|
|
254
|
-
def add_ws( self, path: str,
|
|
255
|
-
route = WS_RouteX(path,
|
|
317
|
+
def add_ws( self, path: str, recv: callable, conn:callable=None, disconn:callable=None, name=None):
|
|
318
|
+
route = WS_RouteX(path, recv=recv, conn=conn, disconn=disconn, name=name, hdrs=self.hdrs, before=self.before, **self.bodykw)
|
|
256
319
|
self.routes.append(route)
|
|
257
320
|
|
|
258
|
-
# %% ../nbs/
|
|
321
|
+
# %% ../nbs/00_core.ipynb 53
|
|
259
322
|
htmxscr = Script(src="https://unpkg.com/htmx.org@next/dist/htmx.min.js")
|
|
260
323
|
htmxwsscr = Script(src="https://unpkg.com/htmx-ext-ws/ws.js")
|
|
261
|
-
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/surreal/surreal.js")
|
|
262
|
-
scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline/script.js")
|
|
324
|
+
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js")
|
|
325
|
+
scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js")
|
|
326
|
+
viewport = Meta(name="viewport", content="width=device-width, initial-scale=1, viewport-fit=cover")
|
|
327
|
+
charset = Meta(charset="utf-8")
|
|
263
328
|
|
|
264
|
-
# %% ../nbs/
|
|
329
|
+
# %% ../nbs/00_core.ipynb 54
|
|
265
330
|
def get_key(key=None, fname='.sesskey'):
|
|
266
331
|
if key: return key
|
|
267
332
|
fname = Path(fname)
|
|
@@ -270,13 +335,16 @@ def get_key(key=None, fname='.sesskey'):
|
|
|
270
335
|
fname.write_text(key)
|
|
271
336
|
return key
|
|
272
337
|
|
|
273
|
-
# %% ../nbs/
|
|
338
|
+
# %% ../nbs/00_core.ipynb 56
|
|
339
|
+
def _list(o): return [] if not o else list(o) if isinstance(o, (tuple,list)) else [o]
|
|
340
|
+
|
|
341
|
+
# %% ../nbs/00_core.ipynb 57
|
|
274
342
|
class FastHTML(Starlette):
|
|
275
343
|
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
|
|
276
|
-
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, before=None, default_hdrs=True,
|
|
277
|
-
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
344
|
+
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, before=None, after=None, default_hdrs=True,
|
|
345
|
+
secret_key=None, session_cookie='session_', max_age=365*24*3600, ws_hdr=False, sess_path='/',
|
|
278
346
|
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', **bodykw):
|
|
279
|
-
middleware,before = _list(middleware),_list(before)
|
|
347
|
+
middleware,before,after = _list(middleware),_list(before),_list(after)
|
|
280
348
|
secret_key = get_key(secret_key, key_fname)
|
|
281
349
|
sess = Middleware(SessionMiddleware, secret_key=secret_key, session_cookie=session_cookie,
|
|
282
350
|
max_age=max_age, path=sess_path, same_site=same_site,
|
|
@@ -284,9 +352,10 @@ class FastHTML(Starlette):
|
|
|
284
352
|
middleware.append(sess)
|
|
285
353
|
super().__init__(debug, routes, middleware, exception_handlers, on_startup, on_shutdown, lifespan=lifespan)
|
|
286
354
|
hdrs = list([] if hdrs is None else hdrs)
|
|
287
|
-
if default_hdrs: hdrs = [htmxscr,
|
|
355
|
+
if default_hdrs: hdrs = [charset, viewport, htmxscr,surrsrc,scopesrc] + hdrs
|
|
356
|
+
if ws_hdr: hdrs.append(htmxwsscr)
|
|
288
357
|
self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, hdrs=hdrs,
|
|
289
|
-
before=before, **bodykw)
|
|
358
|
+
before=before, after=after, **bodykw)
|
|
290
359
|
|
|
291
360
|
def route(self, path:str, methods=None, name=None, include_in_schema=True):
|
|
292
361
|
def f(func):
|
|
@@ -295,20 +364,26 @@ class FastHTML(Starlette):
|
|
|
295
364
|
return func
|
|
296
365
|
return f
|
|
297
366
|
|
|
367
|
+
def ws(self, path:str, conn=None, disconn=None, name=None):
|
|
368
|
+
def f(func):
|
|
369
|
+
self.router.add_ws(path, func, conn=conn, disconn=disconn, name=name)
|
|
370
|
+
return func
|
|
371
|
+
return f
|
|
372
|
+
|
|
298
373
|
all_meths = 'get post put delete patch head trace options'.split()
|
|
299
374
|
for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
|
|
300
375
|
|
|
301
|
-
# %% ../nbs/
|
|
376
|
+
# %% ../nbs/00_core.ipynb 59
|
|
302
377
|
def reg_re_param(m, s):
|
|
303
378
|
cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)
|
|
304
379
|
register_url_convertor(m, cls())
|
|
305
380
|
|
|
306
|
-
# %% ../nbs/
|
|
381
|
+
# %% ../nbs/00_core.ipynb 60
|
|
307
382
|
# Starlette doesn't have the '?', so it chomps the whole remaining URL
|
|
308
383
|
reg_re_param("path", ".*?")
|
|
309
384
|
reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|xml|html")
|
|
310
385
|
|
|
311
|
-
# %% ../nbs/
|
|
386
|
+
# %% ../nbs/00_core.ipynb 61
|
|
312
387
|
class MiddlewareBase:
|
|
313
388
|
async def __call__(self, scope, receive, send) -> None:
|
|
314
389
|
if scope["type"] not in ["http", "websocket"]:
|
|
@@ -24,7 +24,7 @@ def fast_app(db=None, render=None, hdrs=None, tbls=None, before=None, middleware
|
|
|
24
24
|
sess_domain=sess_domain, key_fname=key_fname)
|
|
25
25
|
@app.route("/{fname:path}.{ext:static}")
|
|
26
26
|
async def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}')
|
|
27
|
-
if not db: return app
|
|
27
|
+
if not db: return app,app.route
|
|
28
28
|
|
|
29
29
|
db = database(db)
|
|
30
30
|
if not tbls: tbls={}
|
|
@@ -35,7 +35,7 @@ def fast_app(db=None, render=None, hdrs=None, tbls=None, before=None, middleware
|
|
|
35
35
|
tbls['items'] = kwargs
|
|
36
36
|
dbtbls = [get_tbl(db.t, k, v) for k,v in tbls.items()]
|
|
37
37
|
if len(dbtbls)==1: dbtbls=dbtbls[0]
|
|
38
|
-
return app,*dbtbls
|
|
38
|
+
return app,app.route,*dbtbls
|
|
39
39
|
|
|
40
40
|
def run_uv(fname=None, app='app', host='0.0.0.0', port=None, reload=True):
|
|
41
41
|
glb = inspect.currentframe().f_back.f_globals
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from fasthtml.core import *
|
|
2
|
+
from fasthtml.components import *
|
|
3
|
+
from fasthtml.xtend import *
|
|
4
|
+
|
|
5
|
+
tcid = 'toast-container'
|
|
6
|
+
sk = "toasts"
|
|
7
|
+
toast_css = """
|
|
8
|
+
.toast-container {
|
|
9
|
+
position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000;
|
|
10
|
+
display: flex; flex-direction: column; align-items: center; width: 100%;
|
|
11
|
+
pointer-events: none; opacity: 0; transition: opacity 0.3s ease-in-out;
|
|
12
|
+
}
|
|
13
|
+
.toast {
|
|
14
|
+
background-color: #333; color: white;
|
|
15
|
+
padding: 12px 20px; border-radius: 4px; margin-bottom: 10px;
|
|
16
|
+
max-width: 80%; width: auto; text-align: center;
|
|
17
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
18
|
+
}
|
|
19
|
+
.toast-info { background-color: #2196F3; }
|
|
20
|
+
.toast-success { background-color: #4CAF50; }
|
|
21
|
+
.toast-warning { background-color: #FF9800; }
|
|
22
|
+
.toast-error { background-color: #F44336; }
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
toast_js = """
|
|
26
|
+
export function proc_htmx(sel, func) {
|
|
27
|
+
htmx.onLoad(elt => {
|
|
28
|
+
const elements = any(sel, elt);
|
|
29
|
+
if (elt.matches && elt.matches(sel)) elements.unshift(elt);
|
|
30
|
+
elements.forEach(func);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
proc_htmx('.toast-container', async function(toast) {
|
|
34
|
+
await sleep(100);
|
|
35
|
+
toast.style.opacity = '0.8';
|
|
36
|
+
await sleep(3000);
|
|
37
|
+
toast.style.opacity = '0';
|
|
38
|
+
await sleep(300);
|
|
39
|
+
toast.remove();
|
|
40
|
+
});
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def add_toast(sess, message, typ="info"):
|
|
44
|
+
assert typ in ("info", "success", "warning", "error"), '`typ` not in ("info", "success", "warning", "error")'
|
|
45
|
+
sess.setdefault(sk, []).append((message, typ))
|
|
46
|
+
|
|
47
|
+
def render_toasts(sess):
|
|
48
|
+
toasts = [Div(msg, cls=f"toast toast-{typ}") for msg,typ in sess.pop(sk, [])]
|
|
49
|
+
return Div(Div(*toasts, cls="toast-container"),
|
|
50
|
+
hx_swap_oob="afterbegin:body")
|
|
51
|
+
|
|
52
|
+
def toast_after(resp, req, sess):
|
|
53
|
+
if sk in sess: req.injects.append(render_toasts(sess))
|
|
54
|
+
|
|
55
|
+
def setup_toasts(app):
|
|
56
|
+
app.router.hdrs += (Style(toast_css), Script(toast_js, type="module"))
|
|
57
|
+
app.router.after.append(toast_after)
|
|
58
|
+
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# %% auto 0
|
|
4
4
|
__all__ = ['picocss', 'picolink', 'picocondcss', 'picocondlink', 'set_pico_cls', 'Html', 'A', 'AX', 'Checkbox', 'Card', 'Group',
|
|
5
|
-
'Search', 'Grid', 'DialogX', 'Hidden', 'Container', 'Script', 'Style', 'Titled', 'jsd']
|
|
5
|
+
'Search', 'Grid', 'DialogX', 'Hidden', 'Container', 'Script', 'Style', 'run_js', 'Titled', 'jsd']
|
|
6
6
|
|
|
7
7
|
# %% ../nbs/02_xtend.ipynb 2
|
|
8
8
|
from dataclasses import dataclass, asdict
|
|
@@ -123,20 +123,27 @@ def Script(code:str="", **kwargs)->XT:
|
|
|
123
123
|
|
|
124
124
|
# %% ../nbs/02_xtend.ipynb 30
|
|
125
125
|
@delegates(xt_html, keep=True)
|
|
126
|
-
def Style(
|
|
126
|
+
def Style(*c, **kwargs)->XT:
|
|
127
127
|
"A Style tag that doesn't escape its code"
|
|
128
|
-
return xt_html('style', NotStr
|
|
128
|
+
return xt_html('style', map(NotStr,c), **kwargs)
|
|
129
129
|
|
|
130
130
|
# %% ../nbs/02_xtend.ipynb 31
|
|
131
|
+
def run_js(js, id=None, **kw):
|
|
132
|
+
"Run `js` script, auto-generating `id` based on name of caller if needed, and js-escaping any `kw` params"
|
|
133
|
+
if not id: id = sys._getframe(1).f_code.co_name
|
|
134
|
+
kw = {k:dumps(v) for k,v in kw.items()}
|
|
135
|
+
return Script(js.format(**kw), id=id, hx_swap_oob='true')
|
|
136
|
+
|
|
137
|
+
# %% ../nbs/02_xtend.ipynb 32
|
|
131
138
|
@delegates(xt_hx, keep=True)
|
|
132
139
|
def Titled(title:str="FastHTML app", *args, **kwargs)->XT:
|
|
133
140
|
"An HTML partial containing a `Title`, and `H1`, and any provided children"
|
|
134
141
|
return Title(title), Main(H1(title), *args, cls="container", **kwargs)
|
|
135
142
|
|
|
136
|
-
# %% ../nbs/02_xtend.ipynb
|
|
137
|
-
def jsd(org, repo, root, path, typ='script', ver=None, esm=False, **kwargs)->XT:
|
|
143
|
+
# %% ../nbs/02_xtend.ipynb 33
|
|
144
|
+
def jsd(org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, **kwargs)->XT:
|
|
138
145
|
"jsdelivr `Script` or CSS `Link` tag, or URL"
|
|
139
146
|
ver = '@'+ver if ver else ''
|
|
140
|
-
s = f'https://cdn.jsdelivr.net/
|
|
147
|
+
s = f'https://cdn.jsdelivr.net/{prov}/{org}/{repo}{ver}/{root}/{path}'
|
|
141
148
|
if esm: s += '/+esm'
|
|
142
149
|
return Script(src=s, **kwargs) if typ=='script' else Link(rel='stylesheet', href=s, **kwargs) if typ=='css' else s
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
__all__ = ['picocss', 'picolink', 'picocondcss', 'picocondlink', 'set_pico_cls', 'Html', 'A', 'AX', 'Checkbox', 'Card', 'Group', 'Search', 'Grid', 'DialogX', 'Hidden', 'Container', 'Script', 'Style', 'Titled', 'jsd']
|
|
2
|
+
from dataclasses import dataclass, asdict
|
|
3
|
+
from fastcore.utils import *
|
|
4
|
+
from fastcore.xml import *
|
|
5
|
+
from fastcore.meta import use_kwargs, delegates
|
|
6
|
+
from .components import *
|
|
7
|
+
try:
|
|
8
|
+
from IPython import display
|
|
9
|
+
except ImportError:
|
|
10
|
+
display = None
|
|
11
|
+
picocss = 'https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css'
|
|
12
|
+
picolink = (Link(rel='stylesheet', href=picocss), Style(':root { --pico-font-size: 100%; }'))
|
|
13
|
+
picocondcss = 'https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.conditional.min.css'
|
|
14
|
+
picocondlink = (Link(rel='stylesheet', href=picocondcss), Style(':root { --pico-font-size: 100%; }'))
|
|
15
|
+
|
|
16
|
+
def set_pico_cls():
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def Html(*c, doctype=True, **kwargs) -> XT:
|
|
20
|
+
"""An HTML tag, optionally preceeded by `!DOCTYPE HTML`"""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', 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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
24
|
+
"""An A tag; `href` defaults to '#' for more concise use with HTMX"""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def AX(txt, hx_get=None, target_id=None, hx_swap=None, href='#', *, 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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
28
|
+
"""An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def Checkbox(checked: bool=False, label=None, value='1', *, target_id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
32
|
+
"""A Checkbox optionally inside a Label"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def Card(*c, header=None, footer=None, target_id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
36
|
+
"""A PicoCSS Card, implemented as an Article with optional Header and Footer"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def Group(*c, target_id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
40
|
+
"""A PicoCSS Group, implemented as a Fieldset with role 'group'"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def Search(*c, target_id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
44
|
+
"""A PicoCSS Search, implemented as a Form with role 'search'"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def Grid(*c, cls='grid', target_id=None, id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
48
|
+
"""A PicoCSS Grid, implemented as child Divs in a Div with class 'grid'"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def DialogX(*c, open=None, header=None, footer=None, id=None, target_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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
52
|
+
"""A PicoCSS Dialog, with children inside a Card"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def Hidden(value: str='', *, target_id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
56
|
+
"""An Input of type 'hidden'"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def Container(*args, target_id=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
60
|
+
"""A PicoCSS Container, implemented as a Main with class 'container'"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def Script(code: str='', *, id=None, cls=None, title=None, style=None, **kwargs) -> XT:
|
|
64
|
+
"""A Script tag that doesn't escape its code"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def Style(css: str='', *, id=None, cls=None, title=None, style=None, **kwargs) -> XT:
|
|
68
|
+
"""A Style tag that doesn't escape its code"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def Titled(title: str='FastHTML app', *args, target_id=None, id=None, cls=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs) -> XT:
|
|
72
|
+
"""An HTML partial containing a `Title`, and `H1`, and any provided children"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def jsd(org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, **kwargs) -> XT:
|
|
76
|
+
"""jsdelivr `Script` or CSS `Link` tag, or URL"""
|
|
77
|
+
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: The fastest way to create an HTML app
|
|
5
5
|
Home-page: https://github.com/AnswerDotAI/fasthtml
|
|
6
6
|
Author: Jeremy Howard
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: fastcore>=1.5.46
|
|
19
19
|
Requires-Dist: python-dateutil
|
|
20
|
-
Requires-Dist: starlette
|
|
20
|
+
Requires-Dist: starlette>0.33
|
|
21
21
|
Requires-Dist: oauthlib
|
|
22
22
|
Requires-Dist: itsdangerous
|
|
23
23
|
Requires-Dist: uvicorn[standard]
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[DEFAULT]
|
|
2
2
|
repo = fasthtml
|
|
3
3
|
lib_name = fasthtml
|
|
4
|
-
version = 0.1.
|
|
4
|
+
version = 0.1.2
|
|
5
5
|
min_python = 3.10
|
|
6
6
|
license = apache2
|
|
7
|
-
requirements = fastcore>=1.5.46 python-dateutil starlette oauthlib itsdangerous uvicorn[standard] httpx fastlite>=0.0.6 python-multipart beautifulsoup4
|
|
7
|
+
requirements = fastcore>=1.5.46 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard] httpx fastlite>=0.0.6 python-multipart beautifulsoup4
|
|
8
8
|
dev_requirements = ipython lxml
|
|
9
9
|
black_formatting = False
|
|
10
10
|
conda_user = fastai
|
|
@@ -29,7 +29,7 @@ keywords = nbdev jupyter notebook python
|
|
|
29
29
|
language = English
|
|
30
30
|
status = 3
|
|
31
31
|
console_scripts = fh_railway_link=fasthtml.cli:railway_link
|
|
32
|
-
|
|
32
|
+
fh_railway_deploy=fasthtml.cli:railway_deploy
|
|
33
33
|
user = AnswerDotAI
|
|
34
34
|
readme_nb = index.ipynb
|
|
35
35
|
allowed_metadata_keys =
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import json,dateutil,uuid,inspect
|
|
2
|
-
|
|
3
|
-
from fastcore.utils import *
|
|
4
|
-
from fastcore.xml import *
|
|
5
|
-
from fasthtml.xtend import *
|
|
6
|
-
|
|
7
|
-
from types import UnionType, SimpleNamespace as ns
|
|
8
|
-
from typing import Optional, get_type_hints, get_args, get_origin, Union, Mapping, TypedDict, List
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from dataclasses import dataclass,fields,is_dataclass,MISSING,asdict
|
|
11
|
-
from collections import namedtuple
|
|
12
|
-
from inspect import isfunction,ismethod,signature,Parameter,get_annotations
|
|
13
|
-
from functools import wraps, partialmethod
|
|
14
|
-
|
|
15
|
-
from .starlette import *
|
|
16
|
-
|
|
17
|
-
__all__ = "is_typeddict is_namedtuple date snake2hyphens htmx_hdrs HtmxHeaders str2int HttpHeader form2dict RouteX RouterX FastHTML htmx_hdrs reg_re_param MiddlewareBase Beforeware get_key".split()
|
|
18
|
-
|
|
19
|
-
empty = Parameter.empty
|
|
20
|
-
|
|
21
|
-
def is_typeddict(cls:type)->bool:
|
|
22
|
-
attrs = 'annotations', 'required_keys', 'optional_keys'
|
|
23
|
-
return isinstance(cls, type) and all(hasattr(cls, f'__{attr}__') for attr in attrs)
|
|
24
|
-
|
|
25
|
-
def is_namedtuple(cls):
|
|
26
|
-
"`True` is `cls` is a namedtuple type"
|
|
27
|
-
return issubclass(cls, tuple) and hasattr(cls, '_fields')
|
|
28
|
-
|
|
29
|
-
def date(s:str):
|
|
30
|
-
"Convert `s` to a datetime"
|
|
31
|
-
return dateutil.parser.parse(s)
|
|
32
|
-
|
|
33
|
-
def snake2hyphens(s:str):
|
|
34
|
-
"Convert `s` from snake case to hyphenated and capitalised"
|
|
35
|
-
s = snake2camel(s)
|
|
36
|
-
return camel2words(s, '-')
|
|
37
|
-
|
|
38
|
-
htmx_hdrs = dict(
|
|
39
|
-
boosted="HX-Boosted",
|
|
40
|
-
current_url="HX-Current-URL",
|
|
41
|
-
history_restore_request="HX-History-Restore-Request",
|
|
42
|
-
prompt="HX-Prompt",
|
|
43
|
-
request="HX-Request",
|
|
44
|
-
target="HX-Target",
|
|
45
|
-
trigger_name="HX-Trigger-Name",
|
|
46
|
-
trigger="HX-Trigger")
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class HtmxHeaders:
|
|
50
|
-
boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None
|
|
51
|
-
request:str|None=None; target:str|None=None; trigger_name:str|None=None; trigger:str|None=None
|
|
52
|
-
def __bool__(self): return any(hasattr(self,o) for o in htmx_hdrs)
|
|
53
|
-
|
|
54
|
-
def _get_htmx(req):
|
|
55
|
-
res = {k:req.headers.get(v.lower(), None) for k,v in htmx_hdrs.items()}
|
|
56
|
-
return HtmxHeaders(**res)
|
|
57
|
-
|
|
58
|
-
def str2int(s)->int:
|
|
59
|
-
"Convert `s` to an `int`"
|
|
60
|
-
s = s.lower()
|
|
61
|
-
if s=='on': return 1
|
|
62
|
-
if s=='none': return 0
|
|
63
|
-
return 0 if not s else int(s)
|
|
64
|
-
|
|
65
|
-
def _fix_anno(t):
|
|
66
|
-
"Create appropriate callable type for casting a `str` to type `t` (or first type in `t` if union)"
|
|
67
|
-
origin = get_origin(t)
|
|
68
|
-
if origin is Union or origin is UnionType or origin in (list,List):
|
|
69
|
-
t = first(o for o in get_args(t) if o!=type(None))
|
|
70
|
-
d = {bool: str2bool, int: str2int}
|
|
71
|
-
return d.get(t, t)
|
|
72
|
-
|
|
73
|
-
def _form_arg(k, v, d):
|
|
74
|
-
"Get type by accessing key `k` from `d`, and use to cast `v`"
|
|
75
|
-
if v is None: return
|
|
76
|
-
anno = d.get(k, None)
|
|
77
|
-
if not anno: return v
|
|
78
|
-
anno = _fix_anno(anno)
|
|
79
|
-
if isinstance(v, list): return [anno(o) for o in v]
|
|
80
|
-
return anno(v)
|
|
81
|
-
|
|
82
|
-
def _is_body(anno):
|
|
83
|
-
return issubclass(anno, (dict,ns)) or is_dataclass(anno) or is_namedtuple(anno) or \
|
|
84
|
-
get_annotations(anno) or is_typeddict(anno)
|
|
85
|
-
|
|
86
|
-
def _anno2flds(anno):
|
|
87
|
-
if is_dataclass(anno): return {o.name:o.type for o in fields(anno)}
|
|
88
|
-
if is_namedtuple(anno): return {o:str for o in anno._fields}
|
|
89
|
-
annoanno = get_annotations(anno)
|
|
90
|
-
if annoanno: return annoanno
|
|
91
|
-
return {}
|
|
92
|
-
|
|
93
|
-
def _formitem(form, k):
|
|
94
|
-
o = form.getlist(k)
|
|
95
|
-
return o[0] if len(o) == 1 else o
|
|
96
|
-
|
|
97
|
-
def form2dict(form: FormData) -> dict:
|
|
98
|
-
"Convert starlette form data to a dict"
|
|
99
|
-
return {k: _formitem(form, k) for k in form}
|
|
100
|
-
|
|
101
|
-
async def _from_body(req, arg, p):
|
|
102
|
-
form = await req.form()
|
|
103
|
-
anno = p.annotation
|
|
104
|
-
d = _anno2flds(anno)
|
|
105
|
-
items = form2dict(form).items()
|
|
106
|
-
cargs = {k:_form_arg(k, v, d) for k,v in items}
|
|
107
|
-
return anno(**cargs)
|
|
108
|
-
|
|
109
|
-
async def _find_p(req, arg:str, p):
|
|
110
|
-
anno = p.annotation
|
|
111
|
-
if isinstance(anno, type):
|
|
112
|
-
if issubclass(anno, Request): return req
|
|
113
|
-
if issubclass(anno, HtmxHeaders): return _get_htmx(req)
|
|
114
|
-
if issubclass(anno, Starlette): return req.scope['app']
|
|
115
|
-
if _is_body(anno): return await _from_body(req, arg, p)
|
|
116
|
-
if anno is empty:
|
|
117
|
-
if 'request'.startswith(arg.lower()): return req
|
|
118
|
-
if 'session'.startswith(arg.lower()): return req.scope.get('session', {})
|
|
119
|
-
if arg.lower()=='auth': return req.scope.get('auth', None)
|
|
120
|
-
if arg.lower()=='htmx': return _get_htmx(req)
|
|
121
|
-
if arg.lower()=='app': return req.scope['app']
|
|
122
|
-
return None
|
|
123
|
-
res = req.path_params.get(arg, None)
|
|
124
|
-
if res is empty or res is None: res = req.query_params.get(arg, None)
|
|
125
|
-
if res is empty or res is None: res = req.cookies.get(arg, None)
|
|
126
|
-
if res is empty or res is None: res = req.headers.get(snake2hyphens(arg), None)
|
|
127
|
-
if res is empty or res is None: res = nested_idx(req.scope, 'session', arg) or None
|
|
128
|
-
if res is empty or res is None:
|
|
129
|
-
frm = await req.form()
|
|
130
|
-
res = frm.getlist(arg)
|
|
131
|
-
if res:
|
|
132
|
-
if len(res)==1: res=res[0]
|
|
133
|
-
else: res = None
|
|
134
|
-
if res is empty or res is None: res = p.default
|
|
135
|
-
if not isinstance(res, (list,str)) or anno is empty: return res
|
|
136
|
-
anno = _fix_anno(anno)
|
|
137
|
-
return [anno(o) for o in res] if isinstance(res,list) else anno(res)
|
|
138
|
-
|
|
139
|
-
async def _wrap_req(req, params):
|
|
140
|
-
return [await _find_p(req, arg, p) for arg,p in params.items()]
|
|
141
|
-
|
|
142
|
-
@dataclass
|
|
143
|
-
class HttpHeader: k:str;v:str
|
|
144
|
-
|
|
145
|
-
def flat_xt(lst):
|
|
146
|
-
result = []
|
|
147
|
-
for item in lst:
|
|
148
|
-
if isinstance(item, (list,tuple)) and not isinstance(item, XT): result.extend(item)
|
|
149
|
-
else: result.append(item)
|
|
150
|
-
return result
|
|
151
|
-
|
|
152
|
-
def _xt_resp(req, resp, hdrs, **bodykw):
|
|
153
|
-
if not isinstance(resp, (tuple,list)): resp = (resp,)
|
|
154
|
-
http_hdrs,resp = partition(resp, risinstance(HttpHeader))
|
|
155
|
-
http_hdrs = {o.k:str(o.v) for o in http_hdrs}
|
|
156
|
-
titles,bdy = partition(resp, lambda o: getattr(o, 'tag', '')=='title')
|
|
157
|
-
if resp and 'hx-request' not in req.headers and not any(getattr(o, 'tag', '')=='html' for o in resp):
|
|
158
|
-
if not titles: titles = [Title('FastHTML page')]
|
|
159
|
-
resp = Html(Head(titles[0], *flat_xt(hdrs)), Body(bdy, **bodykw))
|
|
160
|
-
return HTMLResponse(to_xml(resp), headers=http_hdrs)
|
|
161
|
-
|
|
162
|
-
def _wrap_resp(req, resp, cls, hdrs, **bodykw):
|
|
163
|
-
if isinstance(resp, FileResponse) and not os.path.exists(resp.path): raise HTTPException(404, resp.path)
|
|
164
|
-
if isinstance(resp, Response): return resp
|
|
165
|
-
if cls is not empty: return cls(resp)
|
|
166
|
-
if isinstance(resp, (list,tuple)) or hasattr(resp, '__xt__'): return _xt_resp(req, resp, hdrs, **bodykw)
|
|
167
|
-
if isinstance(resp, str): cls = HTMLResponse
|
|
168
|
-
elif isinstance(resp, Mapping): cls = JSONResponse
|
|
169
|
-
else:
|
|
170
|
-
resp = str(resp)
|
|
171
|
-
cls = HTMLResponse
|
|
172
|
-
return cls(resp)
|
|
173
|
-
|
|
174
|
-
class Beforeware:
|
|
175
|
-
def __init__(self, f, skip=None): self.f,self.skip = f,skip or []
|
|
176
|
-
|
|
177
|
-
def _wrap_ep(f, hdrs, before, **bodykw):
|
|
178
|
-
if not (isfunction(f) or ismethod(f)): return f
|
|
179
|
-
sig = signature(f)
|
|
180
|
-
params = sig.parameters
|
|
181
|
-
cls = sig.return_annotation
|
|
182
|
-
|
|
183
|
-
async def _f(req):
|
|
184
|
-
resp = None
|
|
185
|
-
for b in before:
|
|
186
|
-
if not resp:
|
|
187
|
-
if isinstance(b, Beforeware): bf,skip = b.f,b.skip
|
|
188
|
-
else: bf,skip = b,[]
|
|
189
|
-
if not any(re.match(r, req.url.path) for r in skip):
|
|
190
|
-
wreq = await _wrap_req(req, signature(bf).parameters)
|
|
191
|
-
resp = bf(*wreq)
|
|
192
|
-
if is_async_callable(bf): resp = await resp
|
|
193
|
-
if not resp:
|
|
194
|
-
wreq = await _wrap_req(req, params)
|
|
195
|
-
resp = f(*wreq)
|
|
196
|
-
if is_async_callable(f): resp = await resp
|
|
197
|
-
return _wrap_resp(req, resp, cls, hdrs, **bodykw)
|
|
198
|
-
return _f
|
|
199
|
-
|
|
200
|
-
class WS_RouteX(WebSocketRoute):
|
|
201
|
-
def __init__(self, path:str, endpoint, *, name=None, middleware=None, hdrs=None, before=None, **bodykw):
|
|
202
|
-
super().__init__(path, _wrap_ep(endpoint, hdrs, before, **bodykw), name=name, middleware=middleware)
|
|
203
|
-
|
|
204
|
-
class RouteX(Route):
|
|
205
|
-
def __init__(self, path:str, endpoint, *, methods=None, name=None, include_in_schema=True, middleware=None,
|
|
206
|
-
hdrs=None, before=None, **bodykw):
|
|
207
|
-
super().__init__(path, _wrap_ep(endpoint, hdrs, before, **bodykw), methods=methods, name=name,
|
|
208
|
-
include_in_schema=include_in_schema, middleware=middleware)
|
|
209
|
-
|
|
210
|
-
class RouterX(Router):
|
|
211
|
-
def __init__(self, routes=None, redirect_slashes=True, default=None, on_startup=None, on_shutdown=None,
|
|
212
|
-
lifespan=None, *, middleware=None, hdrs=None, before=None, **bodykw):
|
|
213
|
-
super().__init__(routes, redirect_slashes, default, on_startup, on_shutdown,
|
|
214
|
-
lifespan=lifespan, middleware=middleware)
|
|
215
|
-
self.hdrs,self.bodykw,self.before = hdrs or (),bodykw,before or ()
|
|
216
|
-
|
|
217
|
-
def add_route( self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True):
|
|
218
|
-
route = RouteX(path, endpoint=endpoint, methods=methods, name=name, include_in_schema=include_in_schema,
|
|
219
|
-
hdrs=self.hdrs, before=self.before, **self.bodykw)
|
|
220
|
-
self.routes = [o for o in self.routes if getattr(o,'methods',None)!=methods or o.path!=path]
|
|
221
|
-
self.routes.append(route)
|
|
222
|
-
|
|
223
|
-
htmxscr = Script(src="https://unpkg.com/htmx.org@next/dist/htmx.min.js")
|
|
224
|
-
htmxwsscr = Script(src="https://unpkg.com/htmx-ext-ws/ws.js")
|
|
225
|
-
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/surreal/surreal.js")
|
|
226
|
-
scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline/script.js")
|
|
227
|
-
|
|
228
|
-
def get_key(key=None, fname='.sesskey'):
|
|
229
|
-
if key: return key
|
|
230
|
-
fname = Path(fname)
|
|
231
|
-
if fname.exists(): return fname.read_text()
|
|
232
|
-
key = str(uuid.uuid4())
|
|
233
|
-
fname.write_text(key)
|
|
234
|
-
return key
|
|
235
|
-
|
|
236
|
-
def _list(o): return [] if not o else o if isinstance(o, (tuple,list)) else [o]
|
|
237
|
-
|
|
238
|
-
class FastHTML(Starlette):
|
|
239
|
-
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
|
|
240
|
-
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, before=None, default_hdrs=True,
|
|
241
|
-
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
242
|
-
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', **bodykw):
|
|
243
|
-
middleware,before = _list(middleware),_list(before)
|
|
244
|
-
secret_key = get_key(secret_key, key_fname)
|
|
245
|
-
sess = Middleware(SessionMiddleware, secret_key=secret_key, session_cookie=session_cookie,
|
|
246
|
-
max_age=max_age, path=sess_path, same_site=same_site,
|
|
247
|
-
https_only=sess_https_only, domain=sess_domain)
|
|
248
|
-
middleware.append(sess)
|
|
249
|
-
super().__init__(debug, routes, middleware, exception_handlers, on_startup, on_shutdown, lifespan=lifespan)
|
|
250
|
-
hdrs = list([] if hdrs is None else hdrs)
|
|
251
|
-
if default_hdrs: hdrs = [htmxscr,surrsrc,scopesrc] + hdrs
|
|
252
|
-
self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, hdrs=hdrs,
|
|
253
|
-
before=before, **bodykw)
|
|
254
|
-
|
|
255
|
-
def route(self, path:str, methods=None, name=None, include_in_schema=True):
|
|
256
|
-
def f(func):
|
|
257
|
-
m = [methods] if isinstance(methods,str) else [func.__name__] if not methods else methods
|
|
258
|
-
self.router.add_route(path, func, methods=m, name=name, include_in_schema=include_in_schema)
|
|
259
|
-
return func
|
|
260
|
-
return f
|
|
261
|
-
|
|
262
|
-
all_meths = 'get post put delete patch head trace options'.split()
|
|
263
|
-
for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
|
|
264
|
-
|
|
265
|
-
def reg_re_param(m, s):
|
|
266
|
-
cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)
|
|
267
|
-
register_url_convertor(m, cls())
|
|
268
|
-
|
|
269
|
-
# Starlette doesn't have the '?', so it chomps the whole remaining URL
|
|
270
|
-
reg_re_param("path", ".*?")
|
|
271
|
-
reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|xml|html")
|
|
272
|
-
|
|
273
|
-
class MiddlewareBase:
|
|
274
|
-
async def __call__(self, scope, receive, send) -> None:
|
|
275
|
-
if scope["type"] not in ["http", "websocket"]:
|
|
276
|
-
await self.app(scope, receive, send)
|
|
277
|
-
return
|
|
278
|
-
return HTTPConnection(scope)
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
__all__ = ['picocss', 'picolink', 'picocondcss', 'picocondlink', 'set_pico_cls', 'A', 'AX', 'Checkbox', 'Card', 'Group', 'Search', 'Grid', 'DialogX', 'Hidden']
|
|
2
|
-
from html.parser import HTMLParser
|
|
3
|
-
from dataclasses import dataclass, asdict
|
|
4
|
-
from fastcore.utils import *
|
|
5
|
-
from fastcore.xml import *
|
|
6
|
-
from fastcore.meta import use_kwargs, delegates
|
|
7
|
-
from .components import *
|
|
8
|
-
try:
|
|
9
|
-
from IPython import display
|
|
10
|
-
except ImportError:
|
|
11
|
-
display = None
|
|
12
|
-
picocss = 'https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css'
|
|
13
|
-
picolink = Link(rel='stylesheet', href=picocss)
|
|
14
|
-
picocondcss = 'https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.conditional.min.css'
|
|
15
|
-
picocondlink = Link(rel='stylesheet', href=picocondcss)
|
|
16
|
-
|
|
17
|
-
def set_pico_cls():
|
|
18
|
-
...
|
|
19
|
-
|
|
20
|
-
def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', id=None, cls=None, title=None, style=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
21
|
-
...
|
|
22
|
-
|
|
23
|
-
def AX(txt, hx_get=None, target_id=None, hx_swap=None, href='#', *, id=None, cls=None, title=None, style=None, hx_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_target=None, hx_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
24
|
-
...
|
|
25
|
-
|
|
26
|
-
def Checkbox(checked: bool=False, label=None, *, target_id=None, id=None, cls=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
27
|
-
...
|
|
28
|
-
|
|
29
|
-
def Card(*c, header=None, footer=None, target_id=None, id=None, cls=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
30
|
-
...
|
|
31
|
-
|
|
32
|
-
def Group(*c, target_id=None, id=None, cls=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
33
|
-
...
|
|
34
|
-
|
|
35
|
-
def Search(*c, target_id=None, id=None, cls=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
36
|
-
...
|
|
37
|
-
|
|
38
|
-
def Grid(*c, cls='grid', target_id=None, id=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
39
|
-
...
|
|
40
|
-
|
|
41
|
-
def DialogX(*c, open=None, header=None, footer=None, id=None, target_id=None, cls=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
42
|
-
...
|
|
43
|
-
|
|
44
|
-
def Hidden(value: str='', *, target_id=None, id=None, cls=None, title=None, style=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_include=None, hx_select=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_on=None, **kwargs):
|
|
45
|
-
...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|