python-fasthtml 0.1.6__tar.gz → 0.1.8__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.6/python_fasthtml.egg-info → python_fasthtml-0.1.8}/PKG-INFO +1 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/__init__.py +1 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/_modidx.py +2 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/cli.py +2 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/components.py +12 -7
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/components.pyi +2 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/core.py +68 -58
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/fastapp.py +5 -7
- python_fasthtml-0.1.8/fasthtml/xt.py +4 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/xtend.py +4 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/xtend.pyi +1 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8/python_fasthtml.egg-info}/PKG-INFO +1 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/SOURCES.txt +1 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/settings.ini +1 -1
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/LICENSE +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/MANIFEST.in +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/README.md +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/common.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/js.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/oauth.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/svg.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/not-zip-safe +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/requires.txt +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/setup.cfg +0 -0
- {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/setup.py +0 -0
|
@@ -49,6 +49,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
49
49
|
'fasthtml.core._get_htmx': ('core.html#_get_htmx', 'fasthtml/core.py'),
|
|
50
50
|
'fasthtml.core._is_body': ('core.html#_is_body', 'fasthtml/core.py'),
|
|
51
51
|
'fasthtml.core._list': ('core.html#_list', 'fasthtml/core.py'),
|
|
52
|
+
'fasthtml.core._mk_list': ('core.html#_mk_list', 'fasthtml/core.py'),
|
|
52
53
|
'fasthtml.core._send_ws': ('core.html#_send_ws', 'fasthtml/core.py'),
|
|
53
54
|
'fasthtml.core._wrap_ep': ('core.html#_wrap_ep', 'fasthtml/core.py'),
|
|
54
55
|
'fasthtml.core._wrap_req': ('core.html#_wrap_req', 'fasthtml/core.py'),
|
|
@@ -85,6 +86,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
85
86
|
'fasthtml.starlette': {},
|
|
86
87
|
'fasthtml.svg': {},
|
|
87
88
|
'fasthtml.toaster': {},
|
|
89
|
+
'fasthtml.xt': {},
|
|
88
90
|
'fasthtml.xtend': { 'fasthtml.xtend.A': ('xtend.html#a', 'fasthtml/xtend.py'),
|
|
89
91
|
'fasthtml.xtend.AX': ('xtend.html#ax', 'fasthtml/xtend.py'),
|
|
90
92
|
'fasthtml.xtend.Card': ('xtend.html#card', 'fasthtml/xtend.py'),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# %% auto 0
|
|
4
4
|
__all__ = ['railway_link', 'railway_deploy']
|
|
5
5
|
|
|
6
|
-
# %% ../nbs/04_cli.ipynb
|
|
6
|
+
# %% ../nbs/04_cli.ipynb 2
|
|
7
7
|
from fastcore.utils import *
|
|
8
8
|
from fastcore.script import call_parse, bool_arg
|
|
9
9
|
from subprocess import check_output, run
|
|
@@ -34,6 +34,7 @@ def railway_deploy(
|
|
|
34
34
|
name:str, # The project name to deploy
|
|
35
35
|
mount:bool_arg=True # Create a mounted volume at /app/data?
|
|
36
36
|
):
|
|
37
|
+
"""Deploy a FastHTML app to Railway"""
|
|
37
38
|
cp = run("railway status --json".split(), capture_output=True)
|
|
38
39
|
if not cp.returncode: return print("This project is already deployed. Run `railway open`.")
|
|
39
40
|
reqs = Path('requirements.txt')
|
|
@@ -15,7 +15,7 @@ __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
|
-
from bs4 import BeautifulSoup
|
|
18
|
+
from bs4 import BeautifulSoup, Comment
|
|
19
19
|
|
|
20
20
|
from fastcore.utils import *
|
|
21
21
|
from fastcore.xml import *
|
|
@@ -120,10 +120,12 @@ def __getattr__(tag):
|
|
|
120
120
|
return _f
|
|
121
121
|
|
|
122
122
|
# %% ../nbs/01_components.ipynb 24
|
|
123
|
+
_re_h2x_attr_key = re.compile(r'^[A-Za-z_-][\w-]*$')
|
|
123
124
|
def html2xt(html):
|
|
125
|
+
"""Convert HTML to an `xt` expression"""
|
|
124
126
|
rev_map = {'class': 'cls', 'for': 'fr'}
|
|
125
127
|
|
|
126
|
-
def _parse(elm, lvl=0):
|
|
128
|
+
def _parse(elm, lvl=0, indent=4):
|
|
127
129
|
if isinstance(elm, str): return repr(elm.strip()) if elm.strip() else ''
|
|
128
130
|
if isinstance(elm, list): return '\n'.join(_parse(o, lvl) for o in elm)
|
|
129
131
|
tag_name = elm.name.capitalize()
|
|
@@ -132,14 +134,17 @@ def html2xt(html):
|
|
|
132
134
|
cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)
|
|
133
135
|
for c in cts if str(c).strip()]
|
|
134
136
|
attrs = []
|
|
135
|
-
for key, value in elm.attrs.items():
|
|
137
|
+
for key, value in sorted(elm.attrs.items(), key=lambda x: x[0]=='class'):
|
|
136
138
|
if isinstance(value,(tuple,list)): value = " ".join(value)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
key = rev_map.get(key, key)
|
|
140
|
+
attrs.append(f'{key.replace("-", "_")}={value!r}' if _re_h2x_attr_key.match(key) else f'**{{{key!r}:{value!r}}}')
|
|
141
|
+
spc = " "*lvl*indent
|
|
139
142
|
onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))
|
|
140
143
|
j = ', ' if onlychild else f',\n{spc}'
|
|
141
144
|
inner = j.join(filter(None, cs+attrs))
|
|
142
145
|
if onlychild: return f'{tag_name}({inner})'
|
|
143
|
-
return f'{tag_name}(\n{spc}{inner}\n{" "*(lvl-1)*
|
|
146
|
+
return f'{tag_name}(\n{spc}{inner}\n{" "*(lvl-1)*indent})'
|
|
144
147
|
|
|
145
|
-
|
|
148
|
+
soup = BeautifulSoup(html.strip(), 'html.parser')
|
|
149
|
+
for c in soup.find_all(string=risinstance(Comment)): c.extract()
|
|
150
|
+
return _parse(soup, 1)
|
|
@@ -49,8 +49,10 @@ def find_inputs(e, tags='input', **kw):
|
|
|
49
49
|
|
|
50
50
|
def __getattr__(tag):
|
|
51
51
|
...
|
|
52
|
+
_re_h2x_attr_key = re.compile('^[A-Za-z_-][\\w-]*$')
|
|
52
53
|
|
|
53
54
|
def html2xt(html):
|
|
55
|
+
"""Convert HTML to an `xt` expression"""
|
|
54
56
|
...
|
|
55
57
|
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): ...
|
|
56
58
|
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): ...
|
|
@@ -6,14 +6,14 @@ __all__ = ['empty', 'htmx_hdrs', 'htmxscr', 'htmxwsscr', 'surrsrc', 'scopesrc',
|
|
|
6
6
|
'form2dict', 'flat_xt', 'Beforeware', 'WS_RouteX', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'cookie',
|
|
7
7
|
'reg_re_param', 'MiddlewareBase']
|
|
8
8
|
|
|
9
|
-
# %% ../nbs/00_core.ipynb
|
|
9
|
+
# %% ../nbs/00_core.ipynb 4
|
|
10
10
|
import json,dateutil,uuid,inspect,types
|
|
11
11
|
|
|
12
12
|
from fastcore.utils import *
|
|
13
13
|
from fastcore.xml import *
|
|
14
14
|
from .xtend import *
|
|
15
15
|
|
|
16
|
-
from types import UnionType, SimpleNamespace as ns
|
|
16
|
+
from types import UnionType, SimpleNamespace as ns, GenericAlias
|
|
17
17
|
from typing import Optional, get_type_hints, get_args, get_origin, Union, Mapping, TypedDict, List
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
from dataclasses import dataclass,fields,is_dataclass,MISSING,asdict
|
|
@@ -26,29 +26,29 @@ from .starlette import *
|
|
|
26
26
|
|
|
27
27
|
empty = Parameter.empty
|
|
28
28
|
|
|
29
|
-
# %% ../nbs/00_core.ipynb
|
|
29
|
+
# %% ../nbs/00_core.ipynb 6
|
|
30
30
|
def is_typeddict(cls:type)->bool:
|
|
31
31
|
"Check if `cls` is a `TypedDict`"
|
|
32
32
|
attrs = 'annotations', 'required_keys', 'optional_keys'
|
|
33
33
|
return isinstance(cls, type) and all(hasattr(cls, f'__{attr}__') for attr in attrs)
|
|
34
34
|
|
|
35
|
-
# %% ../nbs/00_core.ipynb
|
|
35
|
+
# %% ../nbs/00_core.ipynb 8
|
|
36
36
|
def is_namedtuple(cls):
|
|
37
37
|
"`True` is `cls` is a namedtuple type"
|
|
38
38
|
return issubclass(cls, tuple) and hasattr(cls, '_fields')
|
|
39
39
|
|
|
40
|
-
# %% ../nbs/00_core.ipynb
|
|
40
|
+
# %% ../nbs/00_core.ipynb 10
|
|
41
41
|
def date(s:str):
|
|
42
42
|
"Convert `s` to a datetime"
|
|
43
43
|
return dateutil.parser.parse(s)
|
|
44
44
|
|
|
45
|
-
# %% ../nbs/00_core.ipynb
|
|
45
|
+
# %% ../nbs/00_core.ipynb 12
|
|
46
46
|
def snake2hyphens(s:str):
|
|
47
47
|
"Convert `s` from snake case to hyphenated and capitalised"
|
|
48
48
|
s = snake2camel(s)
|
|
49
49
|
return camel2words(s, '-')
|
|
50
50
|
|
|
51
|
-
# %% ../nbs/00_core.ipynb
|
|
51
|
+
# %% ../nbs/00_core.ipynb 14
|
|
52
52
|
htmx_hdrs = dict(
|
|
53
53
|
boosted="HX-Boosted",
|
|
54
54
|
current_url="HX-Current-URL",
|
|
@@ -69,7 +69,7 @@ def _get_htmx(h):
|
|
|
69
69
|
res = {k:h.get(v.lower(), None) for k,v in htmx_hdrs.items()}
|
|
70
70
|
return HtmxHeaders(**res)
|
|
71
71
|
|
|
72
|
-
# %% ../nbs/00_core.ipynb
|
|
72
|
+
# %% ../nbs/00_core.ipynb 17
|
|
73
73
|
def str2int(s)->int:
|
|
74
74
|
"Convert `s` to an `int`"
|
|
75
75
|
s = s.lower()
|
|
@@ -77,7 +77,10 @@ def str2int(s)->int:
|
|
|
77
77
|
if s=='none': return 0
|
|
78
78
|
return 0 if not s else int(s)
|
|
79
79
|
|
|
80
|
-
# %% ../nbs/00_core.ipynb
|
|
80
|
+
# %% ../nbs/00_core.ipynb 19
|
|
81
|
+
def _mk_list(t, v): return [t(o) for o in v]
|
|
82
|
+
|
|
83
|
+
# %% ../nbs/00_core.ipynb 21
|
|
81
84
|
def _fix_anno(t):
|
|
82
85
|
"Create appropriate callable type for casting a `str` to type `t` (or first type in `t` if union)"
|
|
83
86
|
origin = get_origin(t)
|
|
@@ -88,7 +91,7 @@ def _fix_anno(t):
|
|
|
88
91
|
if origin in (list,List): res = partial(_mk_list, res)
|
|
89
92
|
return res
|
|
90
93
|
|
|
91
|
-
# %% ../nbs/00_core.ipynb
|
|
94
|
+
# %% ../nbs/00_core.ipynb 26
|
|
92
95
|
def _form_arg(k, v, d):
|
|
93
96
|
"Get type by accessing key `k` from `d`, and use to cast `v`"
|
|
94
97
|
if v is None: return
|
|
@@ -97,45 +100,47 @@ def _form_arg(k, v, d):
|
|
|
97
100
|
if not anno: return v
|
|
98
101
|
return _fix_anno(anno)(v)
|
|
99
102
|
|
|
100
|
-
# %% ../nbs/00_core.ipynb
|
|
103
|
+
# %% ../nbs/00_core.ipynb 30
|
|
101
104
|
@dataclass
|
|
102
105
|
class HttpHeader: k:str;v:str
|
|
103
106
|
|
|
104
|
-
# %% ../nbs/00_core.ipynb
|
|
107
|
+
# %% ../nbs/00_core.ipynb 31
|
|
105
108
|
def _annotations(anno):
|
|
106
109
|
"Same as `get_annotations`, but also works on namedtuples"
|
|
107
110
|
if is_namedtuple(anno): return {o:str for o in anno._fields}
|
|
108
111
|
return get_annotations(anno)
|
|
109
112
|
|
|
110
|
-
# %% ../nbs/00_core.ipynb
|
|
113
|
+
# %% ../nbs/00_core.ipynb 32
|
|
111
114
|
def _is_body(anno): return issubclass(anno, (dict,ns)) or _annotations(anno)
|
|
112
115
|
|
|
113
|
-
# %% ../nbs/00_core.ipynb
|
|
116
|
+
# %% ../nbs/00_core.ipynb 33
|
|
114
117
|
def _formitem(form, k):
|
|
115
118
|
"Return single item `k` from `form` if len 1, otherwise return list"
|
|
116
119
|
o = form.getlist(k)
|
|
117
120
|
return o[0] if len(o) == 1 else o if o else None
|
|
118
121
|
|
|
119
|
-
# %% ../nbs/00_core.ipynb
|
|
122
|
+
# %% ../nbs/00_core.ipynb 34
|
|
120
123
|
def form2dict(form: FormData) -> dict:
|
|
121
124
|
"Convert starlette form data to a dict"
|
|
122
125
|
return {k: _formitem(form, k) for k in form}
|
|
123
126
|
|
|
124
|
-
# %% ../nbs/00_core.ipynb
|
|
127
|
+
# %% ../nbs/00_core.ipynb 36
|
|
125
128
|
async def _from_body(req, p):
|
|
126
129
|
form = await req.form()
|
|
127
130
|
anno = p.annotation
|
|
128
131
|
# Get the fields and types of type `anno`, if available
|
|
129
132
|
d = _annotations(anno)
|
|
130
|
-
cargs = {k:_form_arg(k, v, d) for k,v in form2dict(form).items()}
|
|
133
|
+
cargs = {k:_form_arg(k, v, d) for k,v in form2dict(form).items() if not d or k in d}
|
|
131
134
|
return anno(**cargs)
|
|
132
135
|
|
|
133
|
-
# %% ../nbs/00_core.ipynb
|
|
136
|
+
# %% ../nbs/00_core.ipynb 38
|
|
134
137
|
async def _find_p(req, arg:str, p:Parameter):
|
|
135
138
|
"In `req` find param named `arg` of type in `p` (`arg` is ignored for body types)"
|
|
136
139
|
anno = p.annotation
|
|
137
140
|
# If there's an annotation of special types, return object of that type
|
|
138
|
-
|
|
141
|
+
# GenericAlias is a type of typing for iterators like list[int] that is not a class
|
|
142
|
+
if isinstance(anno, type) and not isinstance(anno, GenericAlias):
|
|
143
|
+
if issubclass(anno, Request): return req
|
|
139
144
|
if issubclass(anno, Request): return req
|
|
140
145
|
if issubclass(anno, HtmxHeaders): return _get_htmx(req.headers)
|
|
141
146
|
if issubclass(anno, Starlette): return req.scope['app']
|
|
@@ -150,15 +155,17 @@ async def _find_p(req, arg:str, p:Parameter):
|
|
|
150
155
|
return None
|
|
151
156
|
# Look through path, cookies, headers, session, query, and body in that order
|
|
152
157
|
res = req.path_params.get(arg, None)
|
|
153
|
-
if res
|
|
154
|
-
if res
|
|
155
|
-
if res
|
|
156
|
-
if res
|
|
157
|
-
if res
|
|
158
|
+
if res in (empty,None): res = req.cookies.get(arg, None)
|
|
159
|
+
if res in (empty,None): res = req.headers.get(snake2hyphens(arg), None)
|
|
160
|
+
if res in (empty,None): res = nested_idx(req.scope, 'session', arg) or None
|
|
161
|
+
if res in (empty,None): res = req.query_params.get(arg, None)
|
|
162
|
+
if res in (empty,None):
|
|
158
163
|
frm = await req.form()
|
|
159
164
|
res = _formitem(frm, arg)
|
|
160
|
-
#
|
|
161
|
-
if res
|
|
165
|
+
# Raise 400 error if the param does not include a default
|
|
166
|
+
if (res in (empty,None)) and p.default is empty: raise HTTPException(400, f"Missing required field: {arg}")
|
|
167
|
+
# If we have a default, return that if we have no value
|
|
168
|
+
if res in (empty,None): res = p.default
|
|
162
169
|
# We can cast str and list[str] to types; otherwise just return what we have
|
|
163
170
|
if not isinstance(res, (list,str)) or anno is empty: return res
|
|
164
171
|
anno = _fix_anno(anno)
|
|
@@ -168,7 +175,7 @@ async def _find_p(req, arg:str, p:Parameter):
|
|
|
168
175
|
async def _wrap_req(req, params):
|
|
169
176
|
return [await _find_p(req, arg, p) for arg,p in params.items()]
|
|
170
177
|
|
|
171
|
-
# %% ../nbs/00_core.ipynb
|
|
178
|
+
# %% ../nbs/00_core.ipynb 40
|
|
172
179
|
def flat_xt(lst):
|
|
173
180
|
"Flatten lists, except for `XT`s"
|
|
174
181
|
result = []
|
|
@@ -177,8 +184,8 @@ def flat_xt(lst):
|
|
|
177
184
|
else: result.append(item)
|
|
178
185
|
return result
|
|
179
186
|
|
|
180
|
-
# %% ../nbs/00_core.ipynb
|
|
181
|
-
def _xt_resp(req, resp, hdrs, **bodykw):
|
|
187
|
+
# %% ../nbs/00_core.ipynb 42
|
|
188
|
+
def _xt_resp(req, resp, hdrs, ftrs, **bodykw):
|
|
182
189
|
if not isinstance(resp, tuple): resp = (resp,)
|
|
183
190
|
resp = resp + tuple(req.injects)
|
|
184
191
|
http_hdrs,resp = partition(resp, risinstance(HttpHeader))
|
|
@@ -186,16 +193,17 @@ def _xt_resp(req, resp, hdrs, **bodykw):
|
|
|
186
193
|
titles,bdy = partition(resp, lambda o: getattr(o, 'tag', '') in ('title','meta'))
|
|
187
194
|
if resp and 'hx-request' not in req.headers and not any(getattr(o, 'tag', '')=='html' for o in resp):
|
|
188
195
|
if not titles: titles = [Title('FastHTML page')]
|
|
189
|
-
resp = Html(Head(*titles, *flat_xt(hdrs)), Body(bdy, **bodykw))
|
|
196
|
+
resp = Html(Head(*titles, *flat_xt(hdrs)), Body(bdy, *flat_xt(ftrs), **bodykw))
|
|
190
197
|
return HTMLResponse(to_xml(resp), headers=http_hdrs)
|
|
191
198
|
|
|
192
|
-
# %% ../nbs/00_core.ipynb
|
|
193
|
-
def _wrap_resp(req, resp, cls, hdrs, **bodykw):
|
|
199
|
+
# %% ../nbs/00_core.ipynb 43
|
|
200
|
+
def _wrap_resp(req, resp, cls, hdrs, ftrs, **bodykw):
|
|
194
201
|
if not resp: resp=()
|
|
195
202
|
if isinstance(resp, FileResponse) and not os.path.exists(resp.path): raise HTTPException(404, resp.path)
|
|
196
203
|
if isinstance(resp, Response): return resp
|
|
197
204
|
if cls is not empty: return cls(resp)
|
|
198
|
-
if isinstance(resp, (list,tuple,HttpHeader)) or hasattr(resp, '__xt__'):
|
|
205
|
+
if isinstance(resp, (list,tuple,HttpHeader)) or hasattr(resp, '__xt__'):
|
|
206
|
+
return _xt_resp(req, resp, hdrs=hdrs, ftrs=ftrs, **bodykw)
|
|
199
207
|
if isinstance(resp, str): cls = HTMLResponse
|
|
200
208
|
elif isinstance(resp, Mapping): cls = JSONResponse
|
|
201
209
|
else:
|
|
@@ -203,12 +211,12 @@ def _wrap_resp(req, resp, cls, hdrs, **bodykw):
|
|
|
203
211
|
cls = HTMLResponse
|
|
204
212
|
return cls(resp)
|
|
205
213
|
|
|
206
|
-
# %% ../nbs/00_core.ipynb
|
|
214
|
+
# %% ../nbs/00_core.ipynb 44
|
|
207
215
|
class Beforeware:
|
|
208
216
|
def __init__(self, f, skip=None): self.f,self.skip = f,skip or []
|
|
209
217
|
|
|
210
|
-
# %% ../nbs/00_core.ipynb
|
|
211
|
-
def _wrap_ep(f, hdrs, before, after, **bodykw):
|
|
218
|
+
# %% ../nbs/00_core.ipynb 45
|
|
219
|
+
def _wrap_ep(f, hdrs, ftrs, before, after, **bodykw):
|
|
212
220
|
if not (isfunction(f) or ismethod(f)): return f
|
|
213
221
|
sig = signature(f)
|
|
214
222
|
params = sig.parameters
|
|
@@ -233,10 +241,10 @@ def _wrap_ep(f, hdrs, before, after, **bodykw):
|
|
|
233
241
|
_,*wreq = await _wrap_req(req, signature(a).parameters)
|
|
234
242
|
nr = a(resp, *wreq)
|
|
235
243
|
if nr: resp = nr
|
|
236
|
-
return _wrap_resp(req, resp, cls, hdrs, **bodykw)
|
|
244
|
+
return _wrap_resp(req, resp, cls, hdrs=hdrs, ftrs=ftrs, **bodykw)
|
|
237
245
|
return _f
|
|
238
246
|
|
|
239
|
-
# %% ../nbs/00_core.ipynb
|
|
247
|
+
# %% ../nbs/00_core.ipynb 47
|
|
240
248
|
def _find_wsp(ws, data, hdrs, arg:str, p:Parameter):
|
|
241
249
|
"In `data` find param named `arg` of type in `p` (`arg` is ignored for body types)"
|
|
242
250
|
anno = p.annotation
|
|
@@ -262,7 +270,7 @@ def _wrap_ws(ws, data, params):
|
|
|
262
270
|
hdrs = data.pop('HEADERS', {})
|
|
263
271
|
return [_find_wsp(ws, data, hdrs, arg, p) for arg,p in params.items()]
|
|
264
272
|
|
|
265
|
-
# %% ../nbs/00_core.ipynb
|
|
273
|
+
# %% ../nbs/00_core.ipynb 48
|
|
266
274
|
async def _send_ws(ws, resp):
|
|
267
275
|
if not resp: return
|
|
268
276
|
res = to_xml(resp) if isinstance(resp, (list,tuple)) or hasattr(resp, '__xt__') else resp
|
|
@@ -289,37 +297,37 @@ def _ws_endp(recv, conn=None, disconn=None, hdrs=None, before=None, **bodykw):
|
|
|
289
297
|
cls.on_receive = _recv
|
|
290
298
|
return cls
|
|
291
299
|
|
|
292
|
-
# %% ../nbs/00_core.ipynb
|
|
300
|
+
# %% ../nbs/00_core.ipynb 51
|
|
293
301
|
class WS_RouteX(WebSocketRoute):
|
|
294
302
|
def __init__(self, path:str, recv, conn:callable=None, disconn:callable=None, *,
|
|
295
303
|
name=None, middleware=None, hdrs=None, before=None, **bodykw):
|
|
296
304
|
super().__init__(path, _ws_endp(recv, conn, disconn, hdrs, before, **bodykw), name=name, middleware=middleware)
|
|
297
305
|
|
|
298
|
-
# %% ../nbs/00_core.ipynb
|
|
306
|
+
# %% ../nbs/00_core.ipynb 52
|
|
299
307
|
class RouteX(Route):
|
|
300
308
|
def __init__(self, path:str, endpoint, *, methods=None, name=None, include_in_schema=True, middleware=None,
|
|
301
|
-
hdrs=None, before=None, after=None, **bodykw):
|
|
302
|
-
ep = _wrap_ep(endpoint, hdrs, before=before, after=after, **bodykw)
|
|
309
|
+
hdrs=None, ftrs=None, before=None, after=None, **bodykw):
|
|
310
|
+
ep = _wrap_ep(endpoint, hdrs, ftrs=ftrs, before=before, after=after, **bodykw)
|
|
303
311
|
super().__init__(path, ep, methods=methods, name=name, include_in_schema=include_in_schema, middleware=middleware)
|
|
304
312
|
|
|
305
|
-
# %% ../nbs/00_core.ipynb
|
|
313
|
+
# %% ../nbs/00_core.ipynb 53
|
|
306
314
|
class RouterX(Router):
|
|
307
315
|
def __init__(self, routes=None, redirect_slashes=True, default=None, on_startup=None, on_shutdown=None,
|
|
308
|
-
lifespan=None, *, middleware=None, hdrs=None, before=None, after=None, **bodykw):
|
|
316
|
+
lifespan=None, *, middleware=None, hdrs=None, ftrs=None, before=None, after=None, **bodykw):
|
|
309
317
|
super().__init__(routes, redirect_slashes, default, on_startup, on_shutdown,
|
|
310
318
|
lifespan=lifespan, middleware=middleware)
|
|
311
|
-
self.hdrs,self.bodykw,self.before,self.after = hdrs,bodykw,before,after
|
|
319
|
+
self.hdrs,self.ftrs,self.bodykw,self.before,self.after = hdrs,ftrs,bodykw,before,after
|
|
312
320
|
|
|
313
321
|
def add_route( self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True):
|
|
314
322
|
route = RouteX(path, endpoint=endpoint, methods=methods, name=name, include_in_schema=include_in_schema,
|
|
315
|
-
hdrs=self.hdrs, before=self.before, after=self.after, **self.bodykw)
|
|
323
|
+
hdrs=self.hdrs, ftrs=self.ftrs, before=self.before, after=self.after, **self.bodykw)
|
|
316
324
|
self.routes.append(route)
|
|
317
325
|
|
|
318
326
|
def add_ws( self, path: str, recv: callable, conn:callable=None, disconn:callable=None, name=None):
|
|
319
327
|
route = WS_RouteX(path, recv=recv, conn=conn, disconn=disconn, name=name, hdrs=self.hdrs, before=self.before, **self.bodykw)
|
|
320
328
|
self.routes.append(route)
|
|
321
329
|
|
|
322
|
-
# %% ../nbs/00_core.ipynb
|
|
330
|
+
# %% ../nbs/00_core.ipynb 54
|
|
323
331
|
htmxscr = Script(src="https://unpkg.com/htmx.org@next/dist/htmx.min.js")
|
|
324
332
|
htmxwsscr = Script(src="https://unpkg.com/htmx-ext-ws/ws.js")
|
|
325
333
|
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@1.3.0/surreal.js")
|
|
@@ -327,7 +335,7 @@ scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/s
|
|
|
327
335
|
viewport = Meta(name="viewport", content="width=device-width, initial-scale=1, viewport-fit=cover")
|
|
328
336
|
charset = Meta(charset="utf-8")
|
|
329
337
|
|
|
330
|
-
# %% ../nbs/00_core.ipynb
|
|
338
|
+
# %% ../nbs/00_core.ipynb 55
|
|
331
339
|
def get_key(key=None, fname='.sesskey'):
|
|
332
340
|
if key: return key
|
|
333
341
|
fname = Path(fname)
|
|
@@ -336,13 +344,14 @@ def get_key(key=None, fname='.sesskey'):
|
|
|
336
344
|
fname.write_text(key)
|
|
337
345
|
return key
|
|
338
346
|
|
|
339
|
-
# %% ../nbs/00_core.ipynb
|
|
347
|
+
# %% ../nbs/00_core.ipynb 57
|
|
340
348
|
def _list(o): return [] if not o else list(o) if isinstance(o, (tuple,list)) else [o]
|
|
341
349
|
|
|
342
|
-
# %% ../nbs/00_core.ipynb
|
|
350
|
+
# %% ../nbs/00_core.ipynb 58
|
|
343
351
|
class FastHTML(Starlette):
|
|
344
352
|
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
|
|
345
|
-
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None,
|
|
353
|
+
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,
|
|
354
|
+
before=None, after=None, default_hdrs=True,
|
|
346
355
|
secret_key=None, session_cookie='session_', max_age=365*24*3600, ws_hdr=False, sess_path='/',
|
|
347
356
|
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', **bodykw):
|
|
348
357
|
middleware,before,after = _list(middleware),_list(before),_list(after)
|
|
@@ -353,10 +362,11 @@ class FastHTML(Starlette):
|
|
|
353
362
|
middleware.append(sess)
|
|
354
363
|
super().__init__(debug, routes, middleware, exception_handlers, on_startup, on_shutdown, lifespan=lifespan)
|
|
355
364
|
hdrs = list([] if hdrs is None else hdrs)
|
|
365
|
+
ftrs = list([] if ftrs is None else ftrs)
|
|
356
366
|
if default_hdrs: hdrs = [charset, viewport, htmxscr,surrsrc,scopesrc] + hdrs
|
|
357
367
|
if ws_hdr: hdrs.append(htmxwsscr)
|
|
358
|
-
self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan,
|
|
359
|
-
before=before, after=after, **bodykw)
|
|
368
|
+
self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan,
|
|
369
|
+
hdrs=hdrs, ftrs=ftrs, before=before, after=after, **bodykw)
|
|
360
370
|
|
|
361
371
|
def route(self, path:str, methods=None, name=None, include_in_schema=True):
|
|
362
372
|
def f(func):
|
|
@@ -374,7 +384,7 @@ class FastHTML(Starlette):
|
|
|
374
384
|
all_meths = 'get post put delete patch head trace options'.split()
|
|
375
385
|
for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
|
|
376
386
|
|
|
377
|
-
# %% ../nbs/00_core.ipynb
|
|
387
|
+
# %% ../nbs/00_core.ipynb 60
|
|
378
388
|
def cookie(key: str, value="", max_age=None, expires=None, path="/", domain=None, secure=False, httponly=False, samesite="lax",):
|
|
379
389
|
"Create a 'set-cookie' `HttpHeader`"
|
|
380
390
|
cookie = cookies.SimpleCookie()
|
|
@@ -392,17 +402,17 @@ def cookie(key: str, value="", max_age=None, expires=None, path="/", domain=None
|
|
|
392
402
|
cookie_val = cookie.output(header="").strip()
|
|
393
403
|
return HttpHeader("set-cookie", cookie_val)
|
|
394
404
|
|
|
395
|
-
# %% ../nbs/00_core.ipynb
|
|
405
|
+
# %% ../nbs/00_core.ipynb 61
|
|
396
406
|
def reg_re_param(m, s):
|
|
397
407
|
cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)
|
|
398
408
|
register_url_convertor(m, cls())
|
|
399
409
|
|
|
400
|
-
# %% ../nbs/00_core.ipynb
|
|
410
|
+
# %% ../nbs/00_core.ipynb 62
|
|
401
411
|
# Starlette doesn't have the '?', so it chomps the whole remaining URL
|
|
402
412
|
reg_re_param("path", ".*?")
|
|
403
413
|
reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|xml|html")
|
|
404
414
|
|
|
405
|
-
# %% ../nbs/00_core.ipynb
|
|
415
|
+
# %% ../nbs/00_core.ipynb 63
|
|
406
416
|
class MiddlewareBase:
|
|
407
417
|
async def __call__(self, scope, receive, send) -> None:
|
|
408
418
|
if scope["type"] not in ["http", "websocket"]:
|
|
@@ -12,18 +12,17 @@ def get_tbl(dt, nm, schema):
|
|
|
12
12
|
if render: dc.__xt__ = render
|
|
13
13
|
return tbl,dc
|
|
14
14
|
|
|
15
|
-
def fast_app(db=None, render=None, hdrs=None, tbls=None, before=None, middleware=None, live=False, debug=False, routes=None, exception_handlers=None,
|
|
15
|
+
def fast_app(db=None, render=None, hdrs=None, ftrs=None, tbls=None, before=None, middleware=None, live=False, debug=False, routes=None, exception_handlers=None,
|
|
16
16
|
on_startup=None, on_shutdown=None, lifespan=None, default_hdrs=True, secret_key=None, session_cookie='session_', max_age=365*24*3600,
|
|
17
|
-
sess_path='/', same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', **kwargs):
|
|
17
|
+
pico=None, sess_path='/', same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', bodykw=None, **kwargs):
|
|
18
18
|
|
|
19
|
-
h = ()
|
|
20
|
-
if default_hdrs: h += (picolink, )
|
|
19
|
+
h = (picolink,) if pico or (pico is None and default_hdrs) else ()
|
|
21
20
|
if hdrs: h += tuple(hdrs)
|
|
22
21
|
app_cls = FastHTMLWithLiveReload if live else FastHTML
|
|
23
|
-
app = app_cls(hdrs=h, before=before, middleware=middleware, debug=debug, routes=routes, exception_handlers=exception_handlers,
|
|
22
|
+
app = app_cls(hdrs=h, ftrs=ftrs, before=before, middleware=middleware, debug=debug, routes=routes, exception_handlers=exception_handlers,
|
|
24
23
|
on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, default_hdrs=default_hdrs, secret_key=secret_key,
|
|
25
24
|
session_cookie=session_cookie, max_age=max_age, sess_path=sess_path, same_site=same_site, sess_https_only=sess_https_only,
|
|
26
|
-
sess_domain=sess_domain, key_fname=key_fname)
|
|
25
|
+
sess_domain=sess_domain, key_fname=key_fname, **(bodykw or {}))
|
|
27
26
|
@app.route("/{fname:path}.{ext:static}")
|
|
28
27
|
async def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}')
|
|
29
28
|
if not db: return app,app.route
|
|
@@ -50,4 +49,3 @@ def run_uv(fname=None, app='app', host='0.0.0.0', port=None, reload=True):
|
|
|
50
49
|
def clear(id): return Div(hx_swap_oob='innerHTML', id=id)
|
|
51
50
|
def ContainerX(*cs, **kwargs): return Main(*cs, **kwargs, cls='container', hx_push_url='true', hx_swap_oob='true', id='main')
|
|
52
51
|
def Page(title, *con): return Title(title), ContainerX(H1(title), *con)
|
|
53
|
-
|
|
@@ -184,8 +184,11 @@ def Titled(title:str="FastHTML app", *args, **kwargs)->XT:
|
|
|
184
184
|
return Title(title), Main(H1(title), *args, cls="container", **kwargs)
|
|
185
185
|
|
|
186
186
|
# %% ../nbs/02_xtend.ipynb 39
|
|
187
|
-
def Socials(title, site_name, description, image, url, w=1200, h=630, twitter_site=None, creator=None, card='summary'):
|
|
187
|
+
def Socials(title, site_name, description, image, url=None, w=1200, h=630, twitter_site=None, creator=None, card='summary'):
|
|
188
188
|
"OG and Twitter social card headers"
|
|
189
|
+
if not url: url=site_name
|
|
190
|
+
if not url.startswith('http'): url = f'https://{url}'
|
|
191
|
+
if not image.startswith('http'): image = f'{url}{image}'
|
|
189
192
|
res = [Meta(property='og:image', content=image),
|
|
190
193
|
Meta(property='og:site_name', content=site_name),
|
|
191
194
|
Meta(property='og:image:type', content='image/png'),
|
|
@@ -101,7 +101,7 @@ def Titled(title: str='FastHTML app', *args, target_id=None, id=None, cls=None,
|
|
|
101
101
|
"""An HTML partial containing a `Title`, and `H1`, and any provided children"""
|
|
102
102
|
...
|
|
103
103
|
|
|
104
|
-
def Socials(title, site_name, description, image, url, w=1200, h=630, twitter_site=None, creator=None, card='summary'):
|
|
104
|
+
def Socials(title, site_name, description, image, url=None, w=1200, h=630, twitter_site=None, creator=None, card='summary'):
|
|
105
105
|
"""OG and Twitter social card headers"""
|
|
106
106
|
...
|
|
107
107
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[DEFAULT]
|
|
2
2
|
repo = fasthtml
|
|
3
3
|
lib_name = fasthtml
|
|
4
|
-
version = 0.1.
|
|
4
|
+
version = 0.1.8
|
|
5
5
|
min_python = 3.10
|
|
6
6
|
license = apache2
|
|
7
7
|
requirements = fastcore>=1.5.46 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.0.6 python-multipart beautifulsoup4
|
|
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
|
|
File without changes
|
{python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/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
|
|
File without changes
|