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.
Files changed (33) hide show
  1. {python-fasthtml-0.1.0/python_fasthtml.egg-info → python_fasthtml-0.1.2}/PKG-INFO +2 -2
  2. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/__init__.py +4 -2
  3. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/_modidx.py +47 -40
  4. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/common.py +0 -1
  5. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/components.py +4 -1
  6. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/components.pyi +1 -0
  7. python-fasthtml-0.1.0/fasthtml/test.py → python_fasthtml-0.1.2/fasthtml/core.py +135 -60
  8. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/fastapp.py +2 -2
  9. python_fasthtml-0.1.2/fasthtml/toaster.py +58 -0
  10. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/xtend.py +13 -6
  11. python_fasthtml-0.1.2/fasthtml/xtend.pyi +77 -0
  12. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2/python_fasthtml.egg-info}/PKG-INFO +2 -2
  13. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/SOURCES.txt +1 -1
  14. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/requires.txt +1 -1
  15. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/settings.ini +3 -3
  16. python-fasthtml-0.1.0/fasthtml/core.py +0 -278
  17. python-fasthtml-0.1.0/fasthtml/xtend.pyi +0 -45
  18. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/LICENSE +0 -0
  19. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/MANIFEST.in +0 -0
  20. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/README.md +0 -0
  21. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/authmw.py +0 -0
  22. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/cli.py +0 -0
  23. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/js.py +0 -0
  24. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/live_reload.py +0 -0
  25. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/oauth.py +0 -0
  26. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/starlette.py +0 -0
  27. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/fasthtml/svg.py +0 -0
  28. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  29. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/entry_points.txt +0 -0
  30. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/not-zip-safe +0 -0
  31. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/python_fasthtml.egg-info/top_level.txt +0 -0
  32. {python-fasthtml-0.1.0 → python_fasthtml-0.1.2}/setup.cfg +0 -0
  33. {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.0
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,6 +1,8 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.2"
2
2
  from .core import *
3
3
  from .authmw import *
4
4
  from .components import *
5
5
  from .xtend import *
6
- from .live_reload import *
6
+ from .live_reload import *
7
+ from .toaster import *
8
+
@@ -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.test': { 'fasthtml.test.Beforeware': ('core_tests.html#beforeware', 'fasthtml/test.py'),
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')}}}
@@ -9,4 +9,3 @@ from fastlite import *
9
9
  from . import *
10
10
  from .js import *
11
11
  from .fastapp import *
12
-
@@ -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/core_tests.ipynb.
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', 'all_meths', 'is_typeddict', 'is_namedtuple',
5
- 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', 'form2dict', 'flat_xt', 'Beforeware',
6
- 'WS_RouteX', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'reg_re_param', 'MiddlewareBase']
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/core_tests.ipynb 2
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/core_tests.ipynb 4
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/core_tests.ipynb 6
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/core_tests.ipynb 8
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/core_tests.ipynb 10
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/core_tests.ipynb 12
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(req):
67
- res = {k:req.headers.get(v.lower(), None) for k,v in htmx_hdrs.items()}
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/core_tests.ipynb 15
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/core_tests.ipynb 18
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/core_tests.ipynb 23
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/core_tests.ipynb 27
100
+ # %% ../nbs/00_core.ipynb 29
99
101
  @dataclass
100
102
  class HttpHeader: k:str;v:str
101
103
 
102
- # %% ../nbs/core_tests.ipynb 28
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/core_tests.ipynb 29
110
+ # %% ../nbs/00_core.ipynb 31
109
111
  def _is_body(anno): return issubclass(anno, (dict,ns)) or _annotations(anno)
110
112
 
111
- # %% ../nbs/core_tests.ipynb 30
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/core_tests.ipynb 31
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/core_tests.ipynb 33
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/core_tests.ipynb 35
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/core_tests.ipynb 37
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/core_tests.ipynb 39
179
+ # %% ../nbs/00_core.ipynb 41
178
180
  def _xt_resp(req, resp, hdrs, **bodykw):
179
- if not isinstance(resp, (tuple,list)): resp = (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', '')=='title')
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[0], *flat_xt(hdrs)), Body(bdy, **bodykw))
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/core_tests.ipynb 40
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/core_tests.ipynb 41
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/core_tests.ipynb 42
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/core_tests.ipynb 43
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, endpoint, *, name=None, middleware=None, hdrs=None, before=None, **bodykw):
232
- super().__init__(path, _wrap_ep(endpoint, hdrs, before, **bodykw), name=name, middleware=middleware)
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/core_tests.ipynb 44
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
- super().__init__(path, _wrap_ep(endpoint, hdrs, before, **bodykw), methods=methods, name=name,
239
- include_in_schema=include_in_schema, middleware=middleware)
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/core_tests.ipynb 45
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 or (),bodykw,before or ()
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, endpoint: callable, name=None):
255
- route = WS_RouteX(path, endpoint=endpoint, name=name, hdrs=self.hdrs, before=self.before, **self.bodykw)
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/core_tests.ipynb 46
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/core_tests.ipynb 47
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/core_tests.ipynb 50
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,htmxwsscr,surrsrc,scopesrc] + hdrs
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/core_tests.ipynb 51
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/core_tests.ipynb 52
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/core_tests.ipynb 53
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(css:str="", **kwargs)->XT:
126
+ def Style(*c, **kwargs)->XT:
127
127
  "A Style tag that doesn't escape its code"
128
- return xt_html('style', NotStr(css), **kwargs)
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 32
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/gh/{org}/{repo}{ver}/{root}/{path}'
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.0
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]
@@ -17,7 +17,7 @@ fasthtml/live_reload.py
17
17
  fasthtml/oauth.py
18
18
  fasthtml/starlette.py
19
19
  fasthtml/svg.py
20
- fasthtml/test.py
20
+ fasthtml/toaster.py
21
21
  fasthtml/xtend.py
22
22
  fasthtml/xtend.pyi
23
23
  python_fasthtml.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
1
1
  fastcore>=1.5.46
2
2
  python-dateutil
3
- starlette
3
+ starlette>0.33
4
4
  oauthlib
5
5
  itsdangerous
6
6
  uvicorn[standard]
@@ -1,10 +1,10 @@
1
1
  [DEFAULT]
2
2
  repo = fasthtml
3
3
  lib_name = fasthtml
4
- version = 0.1.0
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
- fh_railway_deploy=fasthtml.cli:railway_deploy
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