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.
Files changed (32) hide show
  1. {python_fasthtml-0.1.6/python_fasthtml.egg-info → python_fasthtml-0.1.8}/PKG-INFO +1 -1
  2. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/__init__.py +1 -1
  3. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/_modidx.py +2 -0
  4. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/cli.py +2 -1
  5. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/components.py +12 -7
  6. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/components.pyi +2 -0
  7. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/core.py +68 -58
  8. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/fastapp.py +5 -7
  9. python_fasthtml-0.1.8/fasthtml/xt.py +4 -0
  10. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/xtend.py +4 -1
  11. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/xtend.pyi +1 -1
  12. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8/python_fasthtml.egg-info}/PKG-INFO +1 -1
  13. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/SOURCES.txt +1 -0
  14. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/settings.ini +1 -1
  15. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/LICENSE +0 -0
  16. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/MANIFEST.in +0 -0
  17. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/README.md +0 -0
  18. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/authmw.py +0 -0
  19. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/common.py +0 -0
  20. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/js.py +0 -0
  21. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/live_reload.py +0 -0
  22. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/oauth.py +0 -0
  23. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/starlette.py +0 -0
  24. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/svg.py +0 -0
  25. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/fasthtml/toaster.py +0 -0
  26. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  27. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/entry_points.txt +0 -0
  28. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/not-zip-safe +0 -0
  29. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/requires.txt +0 -0
  30. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/python_fasthtml.egg-info/top_level.txt +0 -0
  31. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/setup.cfg +0 -0
  32. {python_fasthtml-0.1.6 → python_fasthtml-0.1.8}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.6"
1
+ __version__ = "0.1.8"
2
2
  from .core import *
3
3
  from .authmw import *
4
4
  from .components import *
@@ -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 1
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
- attrs.append(f'{rev_map.get(key, key).replace("-", "_")}={value!r}')
138
- spc = " "*lvl*2
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)*2})'
146
+ return f'{tag_name}(\n{spc}{inner}\n{" "*(lvl-1)*indent})'
144
147
 
145
- return _parse(BeautifulSoup(html.strip(), 'html.parser'), 1)
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 3
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 5
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 7
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 9
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 11
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 13
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 16
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 20
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 25
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 29
103
+ # %% ../nbs/00_core.ipynb 30
101
104
  @dataclass
102
105
  class HttpHeader: k:str;v:str
103
106
 
104
- # %% ../nbs/00_core.ipynb 30
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 31
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 32
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 33
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 35
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 37
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
- if isinstance(anno, type):
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 is empty or res is None: res = req.cookies.get(arg, None)
154
- if res is empty or res is None: res = req.headers.get(snake2hyphens(arg), None)
155
- if res is empty or res is None: res = nested_idx(req.scope, 'session', arg) or None
156
- if res is empty or res is None: res = req.query_params.get(arg, None)
157
- if res is empty or res is None:
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
- # Use default param if needed
161
- if res is empty or res is None: res = p.default
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 39
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 41
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 42
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__'): return _xt_resp(req, resp, hdrs, **bodykw)
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 43
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 44
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 46
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 47
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 50
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 51
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 52
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 53
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 54
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 56
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 57
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, before=None, after=None, default_hdrs=True,
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, hdrs=hdrs,
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 59
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 60
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 61
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 62
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
-
@@ -0,0 +1,4 @@
1
+ from fastcore.xml import *
2
+ from .components import *
3
+ from .xtend import *
4
+
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.1.6
3
+ Version: 0.1.8
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
@@ -18,6 +18,7 @@ fasthtml/oauth.py
18
18
  fasthtml/starlette.py
19
19
  fasthtml/svg.py
20
20
  fasthtml/toaster.py
21
+ fasthtml/xt.py
21
22
  fasthtml/xtend.py
22
23
  fasthtml/xtend.pyi
23
24
  python_fasthtml.egg-info/PKG-INFO
@@ -1,7 +1,7 @@
1
1
  [DEFAULT]
2
2
  repo = fasthtml
3
3
  lib_name = fasthtml
4
- version = 0.1.6
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