python-fasthtml 0.13.3__tar.gz → 0.14.0__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 (39) hide show
  1. {python_fasthtml-0.13.3/python_fasthtml.egg-info → python_fasthtml-0.14.0}/PKG-INFO +2 -2
  2. python_fasthtml-0.14.0/fasthtml/__init__.py +2 -0
  3. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/_modidx.py +4 -1
  4. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/common.py +0 -4
  5. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/components.py +2 -4
  6. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/components.pyi +0 -5
  7. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/core.py +74 -30
  8. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/core.pyi +166 -33
  9. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/fastapp.py +5 -5
  10. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/jupyter.py +7 -7
  11. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/xtend.py +0 -3
  12. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/xtend.pyi +38 -5
  13. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/pyproject.toml +3 -3
  14. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0/python_fasthtml.egg-info}/PKG-INFO +2 -2
  15. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/requires.txt +1 -1
  16. python_fasthtml-0.13.3/fasthtml/__init__.py +0 -2
  17. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/CONTRIBUTING.md +0 -0
  18. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/LICENSE +0 -0
  19. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/MANIFEST.in +0 -0
  20. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/README.md +0 -0
  21. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/authmw.py +0 -0
  22. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/basics.py +0 -0
  23. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/cli.py +0 -0
  24. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/ft.py +0 -0
  25. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/js.py +0 -0
  26. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/katex.js +0 -0
  27. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/live_reload.py +0 -0
  28. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/oauth.py +0 -0
  29. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/pico.py +0 -0
  30. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/starlette.py +0 -0
  31. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/stripe_otp.py +0 -0
  32. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/svg.py +0 -0
  33. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/toaster.py +0 -0
  34. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/SOURCES.txt +0 -0
  35. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  36. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/entry_points.txt +0 -0
  37. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/top_level.txt +0 -0
  38. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/setup.cfg +0 -0
  39. {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/tests/test_toaster.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fasthtml
3
- Version: 0.13.3
3
+ Version: 0.14.0
4
4
  Summary: The fastest way to create an HTML app
5
5
  Author-email: Jeremy Howard and contributors <github@jhoward.fastmail.fm>
6
6
  License: Apache-2.0
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: fastcore>=1.12.16
18
+ Requires-Dist: fastcore>=1.12.45
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: starlette~=1.0
21
21
  Requires-Dist: oauthlib
@@ -0,0 +1,2 @@
1
+ __version__ = "0.14.0"
2
+ from .core import *
@@ -48,13 +48,16 @@ d = { 'settings': { 'branch': 'main',
48
48
  'fasthtml.core.FastHTML': ('api/core.html#fasthtml', 'fasthtml/core.py'),
49
49
  'fasthtml.core.FastHTML.__init__': ('api/core.html#fasthtml.__init__', 'fasthtml/core.py'),
50
50
  'fasthtml.core.FastHTML._add_route': ('api/core.html#fasthtml._add_route', 'fasthtml/core.py'),
51
+ 'fasthtml.core.FastHTML._add_routes': ('api/core.html#fasthtml._add_routes', 'fasthtml/core.py'),
51
52
  'fasthtml.core.FastHTML._add_ws': ('api/core.html#fasthtml._add_ws', 'fasthtml/core.py'),
52
53
  'fasthtml.core.FastHTML._endp': ('api/core.html#fasthtml._endp', 'fasthtml/core.py'),
53
54
  'fasthtml.core.FastHTML.add_route': ('api/core.html#fasthtml.add_route', 'fasthtml/core.py'),
54
55
  'fasthtml.core.FastHTML.add_websocket_route': ( 'api/core.html#fasthtml.add_websocket_route',
55
56
  'fasthtml/core.py'),
57
+ 'fasthtml.core.FastHTML.decode_session': ('api/core.html#fasthtml.decode_session', 'fasthtml/core.py'),
56
58
  'fasthtml.core.FastHTML.devtools_json': ('api/core.html#fasthtml.devtools_json', 'fasthtml/core.py'),
57
59
  'fasthtml.core.FastHTML.get_client': ('api/core.html#fasthtml.get_client', 'fasthtml/core.py'),
60
+ 'fasthtml.core.FastHTML.get_testclient': ('api/core.html#fasthtml.get_testclient', 'fasthtml/core.py'),
58
61
  'fasthtml.core.FastHTML.on_event': ('api/core.html#fasthtml.on_event', 'fasthtml/core.py'),
59
62
  'fasthtml.core.FastHTML.route': ('api/core.html#fasthtml.route', 'fasthtml/core.py'),
60
63
  'fasthtml.core.FastHTML.set_lifespan': ('api/core.html#fasthtml.set_lifespan', 'fasthtml/core.py'),
@@ -104,7 +107,6 @@ d = { 'settings': { 'branch': 'main',
104
107
  'fasthtml.core._LifespanCtx.__init__': ('api/core.html#_lifespanctx.__init__', 'fasthtml/core.py'),
105
108
  'fasthtml.core._add_ids': ('api/core.html#_add_ids', 'fasthtml/core.py'),
106
109
  'fasthtml.core._annotations': ('api/core.html#_annotations', 'fasthtml/core.py'),
107
- 'fasthtml.core._apply_ft': ('api/core.html#_apply_ft', 'fasthtml/core.py'),
108
110
  'fasthtml.core._canonical': ('api/core.html#_canonical', 'fasthtml/core.py'),
109
111
  'fasthtml.core._check_anno': ('api/core.html#_check_anno', 'fasthtml/core.py'),
110
112
  'fasthtml.core._find_p': ('api/core.html#_find_p', 'fasthtml/core.py'),
@@ -124,6 +126,7 @@ d = { 'settings': { 'branch': 'main',
124
126
  'fasthtml.core._params': ('api/core.html#_params', 'fasthtml/core.py'),
125
127
  'fasthtml.core._part_resp': ('api/core.html#_part_resp', 'fasthtml/core.py'),
126
128
  'fasthtml.core._resp': ('api/core.html#_resp', 'fasthtml/core.py'),
129
+ 'fasthtml.core._route_pn': ('api/core.html#_route_pn', 'fasthtml/core.py'),
127
130
  'fasthtml.core._send_ws': ('api/core.html#_send_ws', 'fasthtml/core.py'),
128
131
  'fasthtml.core._to_htmx_header': ('api/core.html#_to_htmx_header', 'fasthtml/core.py'),
129
132
  'fasthtml.core._to_xml': ('api/core.html#_to_xml', 'fasthtml/core.py'),
@@ -1,14 +1,10 @@
1
- import uvicorn
2
1
  from dataclasses import dataclass
3
2
  from typing import Any
4
3
 
5
4
  from .starlette import *
6
5
  from fastcore.utils import *
7
6
  from fastcore.xml import *
8
- from apswutils import Database
9
- from fastlite import *
10
7
  from .basics import *
11
- from .pico import *
12
8
  from .authmw import *
13
9
  from .live_reload import *
14
10
  from .toaster import *
@@ -18,7 +18,6 @@ __all__ = ['named', 'html_attrs', 'hx_attrs', 'hx_evts', 'js_evts', 'hx_attrs_an
18
18
 
19
19
  # %% ../nbs/api/01_components.ipynb #8e2d405b
20
20
  from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING
21
- from bs4 import BeautifulSoup, Comment
22
21
  from typing import Literal, Mapping, Optional
23
22
 
24
23
  from fastcore.utils import *
@@ -29,9 +28,6 @@ from .core import fh_cfg, unqid
29
28
 
30
29
  import types, json
31
30
 
32
- try: from IPython import display
33
- except ImportError: display=None
34
-
35
31
  # %% ../nbs/api/01_components.ipynb #dc101f0f
36
32
  @patch
37
33
  def __str__(self:FT): return self.id if self.id else to_xml(self, indent=False)
@@ -127,6 +123,7 @@ def File(fname):
127
123
  # %% ../nbs/api/01_components.ipynb #7861dfe6
128
124
  def show(ft, *rest, iframe=False, height='auto', style=None):
129
125
  "Renders FT Components into HTML within a Jupyter notebook."
126
+ from IPython import display
130
127
  if isinstance(ft, str): ft = Safe(ft)
131
128
  if rest: ft = (ft,)+rest
132
129
  res = to_xml(ft)
@@ -206,6 +203,7 @@ def __getattr__(tag):
206
203
  _re_h2x_attr_key = re.compile(r'^[A-Za-z_-][\w-]*$')
207
204
  def html2ft(html, attr1st=False):
208
205
  """Convert HTML to an `ft` expression"""
206
+ from bs4 import BeautifulSoup, Comment
209
207
  rev_map = {'class': 'cls', 'for': 'fr'}
210
208
 
211
209
  def _parse(elm, lvl=0, indent=4):
@@ -1,7 +1,6 @@
1
1
  """`ft_html` and `ft_hx` functions to add some conveniences to `ft`, along with a full set of basic HTML components, and functions to work with forms and `FT` conversion"""
2
2
  __all__ = ['named', 'html_attrs', 'hx_attrs', 'hx_evts', 'js_evts', 'hx_attrs_annotations', 'hx_evt_attrs', 'js_evt_attrs', 'evt_attrs', 'attrmap_x', 'ft_html', 'ft_hx', 'File', 'show', 'fill_form', 'fill_dataclass', 'find_inputs', 'html2ft', 'sse_message', 'A', 'Abbr', 'Address', 'Area', 'Article', 'Aside', 'Audio', 'B', 'Base', 'Bdi', 'Bdo', 'Blockquote', 'Body', 'Br', 'Button', 'Canvas', 'Caption', 'Cite', 'Code', 'Col', 'Colgroup', 'Data', 'Datalist', 'Dd', 'Del', 'Details', 'Dfn', 'Dialog', 'Div', 'Dl', 'Dt', 'Em', 'Embed', 'Fencedframe', 'Fieldset', 'Figcaption', 'Figure', 'Footer', 'Form', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Head', 'Header', 'Hgroup', 'Hr', 'I', 'Iframe', 'Img', 'Input', 'Ins', 'Kbd', 'Label', 'Legend', 'Li', 'Link', 'Main', 'Map', 'Mark', 'Menu', 'Meta', 'Meter', 'Nav', 'Noscript', 'Object', 'Ol', 'Optgroup', 'Option', 'Output', 'P', 'Picture', 'PortalExperimental', 'Pre', 'Progress', 'Q', 'Rp', 'Rt', 'Ruby', 'S', 'Samp', 'Script', 'Search', 'Section', 'Select', 'Slot', 'Small', 'Source', 'Span', 'Strong', 'Style', 'Sub', 'Summary', 'Sup', 'Table', 'Tbody', 'Td', 'Template', 'Textarea', 'Tfoot', 'Th', 'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']
3
3
  from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING
4
- from bs4 import BeautifulSoup, Comment
5
4
  from typing import Literal, Mapping, Optional
6
5
  from fastcore.utils import *
7
6
  from fastcore.xml import *
@@ -9,10 +8,6 @@ from fastcore.meta import use_kwargs, delegates
9
8
  from fastcore.test import *
10
9
  from .core import fh_cfg, unqid
11
10
  import types, json
12
- try:
13
- from IPython import display
14
- except ImportError:
15
- display = None
16
11
 
17
12
  @patch
18
13
  def __str__(self: FT):
@@ -12,7 +12,7 @@ __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc',
12
12
  'add_sig_param', 'into', 'MiddlewareBase', 'FtResponse', 'unqid']
13
13
 
14
14
  # %% ../nbs/api/00_core.ipynb #23503b9e
15
- import json,uuid,inspect,types,asyncio,inspect,random,contextlib,httpx,itsdangerous,uvicorn
15
+ import json,uuid,inspect,types,asyncio,inspect,random,contextlib,itsdangerous
16
16
 
17
17
  from fastcore.utils import *
18
18
  from fastcore.xml import *
@@ -20,7 +20,7 @@ from fastcore.meta import use_kwargs_dict,delegates
20
20
  from fastcore.style import S
21
21
 
22
22
  from types import UnionType, SimpleNamespace as ns, GenericAlias
23
- from typing import get_args, get_origin, Union, Mapping, List, Any
23
+ from typing import get_args, get_origin, Union, Mapping, List, Any, Callable
24
24
  from datetime import datetime,date
25
25
  from dataclasses import dataclass
26
26
  from inspect import Parameter,get_annotations
@@ -32,7 +32,7 @@ from warnings import warn
32
32
  from dateutil import parser as dtparse
33
33
  from anyio import from_thread
34
34
  from uuid import uuid4, UUID
35
- from base64 import b64encode
35
+ from base64 import b64encode,b64decode
36
36
  from email.utils import format_datetime
37
37
 
38
38
  from .starlette import *
@@ -167,6 +167,8 @@ async def parse_form(req: Request) -> FormData:
167
167
  async def _from_body(conn, p, data):
168
168
  "Create an instance of the annotated type from pre-parsed `data`"
169
169
  anno = p.annotation
170
+ # param params take precedence
171
+ data = dict(data) | getattr(conn, 'path_params', {})
170
172
  ctor = getattr(anno, '__from_request__', None)
171
173
  if ctor:
172
174
  ps = {k:v for k,v in _params(ctor).items() if k != 'cls'}
@@ -187,10 +189,12 @@ class ApiReturn:
187
189
  # %% ../nbs/api/00_core.ipynb #7cc39ba9
188
190
  class JSONResponse(JSONResponseOrig):
189
191
  "Same as starlette's version, but auto-stringifies non serializable types"
190
- def render(self, content: Any) -> bytes:
191
- res = json.dumps(content, ensure_ascii=False, allow_nan=False, indent=None, separators=(",", ":"), default=str)
192
+ def render(self, content:Any)->bytes:
193
+ def _default(o): return list(o) if is_listy(o) else str(o)
194
+ res = json.dumps(content, ensure_ascii=False, allow_nan=False, indent=None, separators=(",",":"), default=_default)
192
195
  return res.encode("utf-8")
193
196
 
197
+
194
198
  # %% ../nbs/api/00_core.ipynb #5fa96e3a
195
199
  async def _find_p(conn, data, hdrs, arg:str, p:Parameter):
196
200
  "In `data` find param named `arg` of type in `p` (`arg` is ignored for body types)"
@@ -210,7 +214,7 @@ async def _find_p(conn, data, hdrs, arg:str, p:Parameter):
210
214
  if anno is empty:
211
215
  if arg.lower()=='ws' or 'request'.startswith(arg.lower()): return conn
212
216
  if 'session'.startswith(arg.lower()): return conn.scope.get('session', {})
213
- if arg.lower()=='scope': return dict2obj(conn.scope)
217
+ if arg.lower()=='scope': return conn.scope
214
218
  if arg.lower()=='data': return data
215
219
  if arg.lower()=='htmx': return _get_htmx(hdrs)
216
220
  if arg.lower()=='app': return conn.scope['app']
@@ -369,16 +373,8 @@ def _find_targets(req, resp):
369
373
  t = resp.attrs.pop(k, None)
370
374
  if t: resp.attrs[v] = _url_for(req, t)
371
375
 
372
- def _apply_ft(o):
373
- "Apply FastTag transformation recursively to object `o`"
374
- if isinstance(o, tuple): o = tuple(_apply_ft(c) for c in o)
375
- if hasattr(o, '__ft__'): o = o.__ft__()
376
- if isinstance(o, FT): o.children = tuple(_apply_ft(c) for c in o.children)
377
- return o
378
-
379
376
  def _to_xml(req, resp, indent):
380
377
  "Convert response to XML string with target URL resolution"
381
- resp = _apply_ft(resp)
382
378
  _find_targets(req, resp)
383
379
  return to_xml(resp, indent=indent)
384
380
 
@@ -670,7 +666,7 @@ all_meths = 'get post put delete patch head trace options'.split()
670
666
 
671
667
  # %% ../nbs/api/00_core.ipynb #26b147ba
672
668
  @patch
673
- def _endp(self:FastHTML, f, body_wrap):
669
+ def _endp(self:FastHTML, f, body_wrap, before:Optional[Callable|tuple]=None):
674
670
  "Create endpoint wrapper with before/after middleware processing"
675
671
  sig = signature_ex(f, True)
676
672
  for n,p in sig.parameters.items(): (msg:=_check_anno(n,p.annotation)) and warn(msg)
@@ -685,6 +681,8 @@ def _endp(self:FastHTML, f, body_wrap):
685
681
  else: bf,skip = b,[]
686
682
  if not any(re.fullmatch(r, req.url.path) for r in skip):
687
683
  resp = await _wrap_call(bf, req, _params(bf))
684
+ for b in listify(before):
685
+ if not resp: resp = await _wrap_call(b, req, _params(b))
688
686
  req.body_wrap = body_wrap
689
687
  if not resp: resp = await _wrap_call(f, req, sig.parameters)
690
688
  for a in self.after:
@@ -737,17 +735,37 @@ def nested_name(f):
737
735
  "Get name of function `f` using '_' to join nested function names"
738
736
  return f.__qualname__.replace('.<locals>.', '_')
739
737
 
740
- # %% ../nbs/api/00_core.ipynb #72760b09
738
+ # %% ../nbs/api/00_core.ipynb #daafe4fc
739
+ @patch
740
+ def _add_routes(self:FastHTML, cls, path, methods, name, include_in_schema, body_wrap, host=None, before:Optional[Callable|tuple]=None):
741
+ "Add HTTP routes from methods on endpoint class `cls`"
742
+ assert not methods, '`methods` is not supported for class route groups; define HTTP methods as class methods instead'
743
+ lf = _mk_locfunc(cls, path, app=self)
744
+ lf.__routename__ = name
745
+ for meth in all_meths:
746
+ handler = getattr(cls, meth, None)
747
+ if handler: self._add_route(handler, path, meth, name, include_in_schema, body_wrap, host=host, before=before)
748
+ return lf
749
+
750
+ # %% ../nbs/api/00_core.ipynb #3710e48b
751
+ def _route_pn(func, path, name):
752
+ "Infer route name/function name/path from an endpoint or endpoint class"
753
+ fn = nested_name(func)
754
+ if isinstance(func, type): fn = fn[0].lower()+fn[1:]
755
+ p = None if callable(path) else path
756
+ if not name: name = fn
757
+ if not p: p = '/'+('' if fn=='index' else fn)
758
+ return name,fn,p
759
+
741
760
  @patch
742
- def _add_route(self:FastHTML, func, path, methods, name, include_in_schema, body_wrap, host=None):
761
+ def _add_route(self:FastHTML, func, path, methods, name, include_in_schema, body_wrap, host=None, before:Optional[Callable|tuple]=None):
743
762
  "Add HTTP route to FastHTML app with automatic method detection"
744
- n,fn,p = name,nested_name(func),None if callable(path) else path
763
+ n,fn,p = _route_pn(func, path, name)
764
+ if isinstance(func, type): return self._add_routes(func, p, methods, n, include_in_schema, body_wrap, host=host, before=before)
745
765
  if methods: m = [methods] if isinstance(methods,str) else methods
746
766
  elif fn in all_meths and p is not None: m = [fn]
747
767
  else: m = ['get','post']
748
- if not n: n = fn
749
- if not p: p = '/'+('' if fn=='index' else fn)
750
- endp = self._endp(func, body_wrap or self.body_wrap)
768
+ endp = self._endp(func, body_wrap or self.body_wrap, before=before)
751
769
  route = HostRoute(p, endpoint=endp, methods=m, name=n, include_in_schema=include_in_schema, host=host)
752
770
  self.add_route(route)
753
771
  lf = _mk_locfunc(func, p, app=self)
@@ -756,10 +774,10 @@ def _add_route(self:FastHTML, func, path, methods, name, include_in_schema, body
756
774
 
757
775
  # %% ../nbs/api/00_core.ipynb #f5cb2c2b
758
776
  @patch
759
- def route(self:FastHTML, path:str=None, methods=None, name=None, include_in_schema=True, body_wrap=None, host=None):
777
+ def route(self:FastHTML, path:str=None, methods=None, name=None, include_in_schema=True, body_wrap=None, host=None, before:Optional[Callable|tuple]=None):
760
778
  "Add a route at `path`"
761
779
  def f(func):
762
- return self._add_route(func, path, methods, name=name, include_in_schema=include_in_schema, body_wrap=body_wrap, host=host)
780
+ return self._add_route(func, path, methods, name=name, include_in_schema=include_in_schema, body_wrap=body_wrap, host=host, before=before)
763
781
  return f(path) if callable(path) else f
764
782
 
765
783
  for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
@@ -772,7 +790,6 @@ def set_lifespan(self:FastHTML, value):
772
790
  self.router.lifespan_context = value
773
791
 
774
792
  # %% ../nbs/api/00_core.ipynb #3a348474
775
- @delegates(uvicorn.run)
776
793
  def serve(
777
794
  appname=None, # Name of the module
778
795
  app='app', # App instance to be served
@@ -782,6 +799,7 @@ def serve(
782
799
  **kwargs
783
800
  ):
784
801
  "Run the app in an async server, with live reload set as the default."
802
+ from uvicorn import run
785
803
  bk = inspect.currentframe().f_back
786
804
  glb = bk.f_globals
787
805
  code = bk.f_code
@@ -798,6 +816,7 @@ def serve(
798
816
  class Client:
799
817
  "A simple httpx ASGI client that doesn't require `async`"
800
818
  def __init__(self, app, url="http://testserver"):
819
+ import httpx
801
820
  self.cli = httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url=url)
802
821
 
803
822
  def _sync(self, method, url, **kwargs):
@@ -825,20 +844,20 @@ class APIRouter:
825
844
  self.prefix = prefix if prefix else ""
826
845
  self.body_wrap = body_wrap
827
846
 
828
- def _wrap_func(self, func, path=None):
829
- name = func.__name__
847
+ def _wrap_func(self, func, path=None, name=None):
830
848
  wrapped = _mk_locfunc(func, path)
831
849
  wrapped.__routename__ = name
832
- # If you are using the def get or def post method names, this approach is not supported
850
+ # If you are using def get/post/etc method names, this approach is not supported
833
851
  if name not in all_meths: setattr(self.rt_funcs, name, wrapped)
834
852
  return wrapped
835
853
 
836
854
  def __call__(self, path:str=None, methods=None, name=None, include_in_schema=True, body_wrap=None):
837
855
  "Add a route at `path`"
838
856
  def f(func):
839
- p = self.prefix + ("/" + ('' if path.__name__=='index' else func.__name__) if callable(path) else path)
840
- wrapped = self._wrap_func(func, p)
841
- self.routes.append((func, p, methods, name, include_in_schema, body_wrap or self.body_wrap))
857
+ n,_,p = _route_pn(func, path, name)
858
+ p = self.prefix + p
859
+ wrapped = self._wrap_func(func, p, n)
860
+ self.routes.append((func, p, methods, n, include_in_schema, body_wrap or self.body_wrap))
842
861
  return wrapped
843
862
  return f(path) if callable(path) else f
844
863
 
@@ -1007,9 +1026,34 @@ def devtools_json(self:FastHTML, path=None, uuid=None):
1007
1026
  @patch
1008
1027
  def get_client(self:FastHTML, asink=False, **kw):
1009
1028
  "Get an httpx client with session cookes set from `**kw`"
1029
+ import httpx
1010
1030
  signer = itsdangerous.TimestampSigner(self.secret_key)
1011
1031
  data = b64encode(dumps(kw).encode())
1012
1032
  data = signer.sign(data)
1013
1033
  client = httpx.AsyncClient() if asink else httpx.Client()
1014
1034
  client.cookies.update({self.session_cookie: data.decode()})
1015
1035
  return client
1036
+
1037
+ # %% ../nbs/api/00_core.ipynb #c845f437
1038
+ @patch
1039
+ def decode_session(self:FastHTML, cookie):
1040
+ "Decode a signed session cookie"
1041
+ if not cookie or cookie == 'null': return {}
1042
+ unsigned = itsdangerous.TimestampSigner(self.secret_key).unsign(cookie, max_age=None)
1043
+ return loads(b64decode(unsigned).decode())
1044
+
1045
+ # %% ../nbs/api/00_core.ipynb #49260e9b
1046
+ @patch
1047
+ def get_testclient(self:FastHTML, **kw):
1048
+ "Get a Starlette `TestClient` with session cookies set from `**kw`"
1049
+ from starlette.testclient import TestClient
1050
+
1051
+ class FastHTMLTestClient(TestClient):
1052
+ "A Starlette TestClient with a `session` property"
1053
+ @property
1054
+ def session(self):
1055
+ cookie = next((c.value for c in reversed(list(self.cookies.jar))
1056
+ if c.name == self.app.session_cookie), None)
1057
+ return self.app.decode_session(cookie)
1058
+
1059
+ return FastHTMLTestClient(self, cookies=self.get_client(**kw).cookies)
@@ -1,25 +1,25 @@
1
- """The `FastHTML` subclass of `Starlette`, along with the `RouterX` and `RouteX` classes it automatically uses."""
2
- __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date', 'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'JSONResponse', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple', 'noop_body', 'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'FastHTML', 'nested_name', 'serve', 'Client', 'RouteFuncs', 'APIRouter', 'cookie', 'reg_re_param', 'MiddlewareBase', 'FtResponse', 'unqid']
3
- import json, uuid, inspect, types, signal, asyncio, threading, inspect, random, contextlib
1
+ """The `FastHTML` subclass of `Starlette`."""
2
+ __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date', 'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn', 'JSONResponse', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple', 'noop_body', 'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML', 'HostRoute', 'nested_name', 'serve', 'Client', 'RouteFuncs', 'APIRouter', 'cookie', 'reg_re_param', 'StaticNoCache', 'add_sig_param', 'into', 'MiddlewareBase', 'FtResponse', 'unqid']
3
+ import json, uuid, inspect, types, asyncio, inspect, random, contextlib, itsdangerous
4
4
  from fastcore.utils import *
5
5
  from fastcore.xml import *
6
- from fastcore.meta import use_kwargs_dict
6
+ from fastcore.meta import use_kwargs_dict, delegates
7
+ from fastcore.style import S
7
8
  from types import UnionType, SimpleNamespace as ns, GenericAlias
8
- from typing import Optional, get_type_hints, get_args, get_origin, Union, Mapping, TypedDict, List, Any
9
+ from typing import get_args, get_origin, Union, Mapping, List, Any, Callable
9
10
  from datetime import datetime, date
10
- from dataclasses import dataclass, fields
11
- from collections import namedtuple
12
- from inspect import isfunction, ismethod, Parameter, get_annotations
13
- from functools import wraps, partialmethod, update_wrapper
11
+ from dataclasses import dataclass
12
+ from inspect import Parameter, get_annotations
13
+ from functools import partialmethod, update_wrapper
14
14
  from http import cookies
15
15
  from urllib.parse import urlencode, parse_qs, quote, unquote
16
- from copy import copy, deepcopy
16
+ from copy import deepcopy
17
17
  from warnings import warn
18
18
  from dateutil import parser as dtparse
19
- from httpx import ASGITransport, AsyncClient
20
19
  from anyio import from_thread
21
20
  from uuid import uuid4, UUID
22
- from base64 import b85encode, b64encode
21
+ from base64 import b64encode, b64decode
22
+ from email.utils import format_datetime
23
23
  from .starlette import *
24
24
 
25
25
  def _params(f):
@@ -55,6 +55,11 @@ def _get_htmx(h):
55
55
  def _mk_list(t, v):
56
56
  ...
57
57
  fh_cfg = AttrDict(indent=True)
58
+ _special_names = {'ws', 'request', 'session', 'scope', 'data', 'htmx', 'app', 'state', 'auth', 'send', 'api', 'body', 'hdrs', 'ftrs', 'bodykw', 'htmlkw', 'resp', 'self'}
59
+
60
+ def _check_anno(arg, anno):
61
+ """Check for common annotation issues; returns warning string or None"""
62
+ ...
58
63
 
59
64
  def _fix_anno(t, o):
60
65
  """Create appropriate callable type for casting a `str` to type `t` (or first type in `t` if union)"""
@@ -97,17 +102,36 @@ async def parse_form(req: Request) -> FormData:
97
102
  """Starlette errors on empty multipart forms, so this checks for that situation"""
98
103
  ...
99
104
 
100
- async def _from_body(req, p):
105
+ async def _from_body(conn, p, data):
106
+ """Create an instance of the annotated type from pre-parsed `data`"""
101
107
  ...
102
108
 
109
+ class ApiReturn:
110
+
111
+ @classmethod
112
+ async def __from_request__(cls, data, req):
113
+ ...
114
+
115
+ def __init__(self, isapi=False):
116
+ ...
117
+
118
+ def __call__(self, norm=None, **kw):
119
+ ...
120
+
121
+ def __bool__(self):
122
+ ...
123
+
103
124
  class JSONResponse(JSONResponseOrig):
104
125
  """Same as starlette's version, but auto-stringifies non serializable types"""
105
126
 
106
127
  def render(self, content: Any) -> bytes:
107
128
  ...
108
129
 
109
- async def _find_p(req, arg: str, p: Parameter):
110
- """In `req` find param named `arg` of type in `p` (`arg` is ignored for body types)"""
130
+ async def _find_p(conn, data, hdrs, arg: str, p: Parameter):
131
+ """In `data` find param named `arg` of type in `p` (`arg` is ignored for body types)"""
132
+ ...
133
+
134
+ async def _find_ps(conn, data, hdrs, params):
111
135
  ...
112
136
 
113
137
  async def _wrap_req(req, params):
@@ -122,14 +146,13 @@ class Beforeware:
122
146
  def __init__(self, f, skip=None):
123
147
  ...
124
148
 
125
- async def _handle(f, args, **kwargs):
126
- ...
149
+ def __repr__(self):
150
+ ...
127
151
 
128
- def _find_wsp(ws, data, hdrs, arg: str, p: Parameter):
129
- """In `data` find param named `arg` of type in `p` (`arg` is ignored for body types)"""
152
+ async def _handle(f, *args, **kwargs):
130
153
  ...
131
154
 
132
- def _wrap_ws(ws, data, params):
155
+ async def _wrap_ws(ws, data, params):
133
156
  ...
134
157
 
135
158
  async def _send_ws(ws, resp):
@@ -146,9 +169,11 @@ def signal_shutdown():
146
169
  ...
147
170
 
148
171
  def uri(_arg, **kwargs):
172
+ """Create a URI by URL-encoding `_arg` and appending query parameters from `kwargs`"""
149
173
  ...
150
174
 
151
175
  def decode_uri(s):
176
+ """Decode a URI created by `uri()` back into argument and keyword dict"""
152
177
  ...
153
178
  from starlette.convertors import StringConvertor
154
179
  StringConvertor.regex = '[^/]*'
@@ -160,23 +185,23 @@ def to_string(self: StringConvertor, value: str) -> str:
160
185
  @patch
161
186
  def url_path_for(self: HTTPConnection, name: str, **path_params):
162
187
  ...
163
- _verbs = dict(get='hx-get', post='hx-post', put='hx-post', delete='hx-delete', patch='hx-patch', link='href')
188
+ _verbs = dict(get='hx-get', post='hx-post', put='hx-put', delete='hx-delete', patch='hx-patch', link='href')
164
189
 
165
190
  def _url_for(req, t):
191
+ """Generate URL for route `t` using request `req`"""
166
192
  ...
167
193
 
168
194
  def _find_targets(req, resp):
169
- ...
170
-
171
- def _apply_ft(o):
195
+ """Find and convert route targets in response attributes to URLs"""
172
196
  ...
173
197
 
174
198
  def _to_xml(req, resp, indent):
199
+ """Convert response to XML string with target URL resolution"""
175
200
  ...
176
201
  _iter_typs = (tuple, list, map, filter, range, types.GeneratorType)
177
202
 
178
203
  def flat_tuple(o):
179
- """Flatten lists"""
204
+ """Flatten nested iterables into a single tuple"""
180
205
  ...
181
206
 
182
207
  def noop_body(c, req):
@@ -188,18 +213,26 @@ def respond(req, heads, bdy):
188
213
  ...
189
214
 
190
215
  def is_full_page(req, resp):
216
+ """Check if response should be rendered as full page or fragment"""
191
217
  ...
192
218
 
193
219
  def _part_resp(req, resp):
220
+ """Partition response into HTTP headers, background tasks, and content"""
221
+ ...
222
+
223
+ def _canonical(req):
194
224
  ...
195
225
 
196
226
  def _xt_cts(req, resp):
227
+ """Extract content and headers, render as full page or fragment"""
197
228
  ...
198
229
 
199
230
  def _is_ft_resp(resp):
231
+ """Check if response is a FastTag-compatible type"""
200
232
  ...
201
233
 
202
234
  def _resp(req, resp, cls=empty, status_code=200):
235
+ """Create appropriate HTTP response from request and response data"""
203
236
  ...
204
237
 
205
238
  class Redirect:
@@ -212,9 +245,10 @@ class Redirect:
212
245
  ...
213
246
 
214
247
  async def _wrap_call(f, req, params):
248
+ """Wrap function call with request parameter injection"""
215
249
  ...
216
- htmx_exts = {'morph': 'https://cdn.jsdelivr.net/npm/idiomorph@0.7.3/dist/idiomorph-ext.min.js', 'head-support': 'https://cdn.jsdelivr.net/npm/htmx-ext-head-support@2.0.3/head-support.js', 'preload': 'https://cdn.jsdelivr.net/npm/htmx-ext-preload@2.1.0/preload.js', 'class-tools': 'https://cdn.jsdelivr.net/npm/htmx-ext-class-tools@2.0.1/class-tools.js', 'loading-states': 'https://cdn.jsdelivr.net/npm/htmx-ext-loading-states@2.0.0/loading-states.js', 'multi-swap': 'https://cdn.jsdelivr.net/npm/htmx-ext-multi-swap@2.0.0/multi-swap.js', 'path-deps': 'https://cdn.jsdelivr.net/npm/htmx-ext-path-deps@2.0.0/path-deps.js', 'remove-me': 'https://cdn.jsdelivr.net/npm/htmx-ext-remove-me@2.0.0/remove-me.js', 'ws': 'https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.3/ws.js', 'chunked-transfer': 'https://cdn.jsdelivr.net/npm/htmx-ext-transfer-encoding-chunked@0.4.0/transfer-encoding-chunked.js'}
217
- htmxsrc = Script(src='https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js')
250
+ htmx_exts = {'morph': 'https://cdn.jsdelivr.net/npm/idiomorph@0.7.3/dist/idiomorph-ext.min.js', 'head-support': 'https://cdn.jsdelivr.net/npm/htmx-ext-head-support@2.0.4/head-support.js', 'preload': 'https://cdn.jsdelivr.net/npm/htmx-ext-preload@2.1.1/preload.js', 'class-tools': 'https://cdn.jsdelivr.net/npm/htmx-ext-class-tools@2.0.1/class-tools.js', 'loading-states': 'https://cdn.jsdelivr.net/npm/htmx-ext-loading-states@2.0.1/loading-states.js', 'multi-swap': 'https://cdn.jsdelivr.net/npm/htmx-ext-multi-swap@2.0.0/multi-swap.js', 'path-deps': 'https://cdn.jsdelivr.net/npm/htmx-ext-path-deps@2.0.0/path-deps.js', 'remove-me': 'https://cdn.jsdelivr.net/npm/htmx-ext-remove-me@2.0.0/remove-me.js', 'debug': 'https://unpkg.com/htmx.org@1.9.12/dist/ext/debug.js', 'ws': 'https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.3/ws.js', 'chunked-transfer': 'https://cdn.jsdelivr.net/npm/htmx-ext-transfer-encoding-chunked@0.4.0/transfer-encoding-chunked.js'}
251
+ htmxsrc = Script(src='https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js')
218
252
  fhjsscr = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js')
219
253
  surrsrc = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js')
220
254
  scopesrc = Script(src='https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js')
@@ -222,12 +256,15 @@ viewport = Meta(name='viewport', content='width=device-width, initial-scale=1, v
222
256
  charset = Meta(charset='utf-8')
223
257
 
224
258
  def get_key(key=None, fname='.sesskey'):
259
+ """Get session key from `key` param or read/create from file `fname`"""
225
260
  ...
226
261
 
227
262
  def _list(o):
263
+ """Wrap non-list item in a list, returning empty list if None"""
228
264
  ...
229
265
 
230
266
  def _wrap_ex(f, status_code, hdrs, ftrs, htmlkw, bodykw, body_wrap):
267
+ """Wrap exception handler with FastHTML request processing"""
231
268
  ...
232
269
 
233
270
  def qp(p: str, **kw) -> str:
@@ -240,32 +277,79 @@ def def_hdrs(htmx=True, surreal=True):
240
277
  cors_allow = Middleware(CORSMiddleware, allow_credentials=True, allow_origins=['*'], allow_methods=['*'], allow_headers=['*'])
241
278
  iframe_scr = Script(NotStr("\n function sendmsg() {\n window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');\n }\n window.onload = function() {\n sendmsg();\n document.body.addEventListener('htmx:afterSettle', sendmsg);\n document.body.addEventListener('htmx:wsAfterMessage', sendmsg);\n };"))
242
279
 
280
+ class _LifespanCtx:
281
+
282
+ def __init__(self, gen):
283
+ ...
284
+
285
+ async def __aenter__(self):
286
+ ...
287
+
288
+ async def __aexit__(self, *_):
289
+ ...
290
+
291
+ def __aiter__(self):
292
+ ...
293
+
294
+ async def __anext__(self):
295
+ ...
296
+
297
+ class Lifespan:
298
+
299
+ def __init__(self, startup=None, shutdown=None, ls=None):
300
+ ...
301
+
302
+ def __call__(self, app):
303
+ ...
304
+
305
+ async def _run(self, app):
306
+ ...
307
+
308
+ def on_event(self, event_type):
309
+ ...
310
+
243
311
  class FastHTML(Starlette):
244
312
 
245
313
  def __init__(self, debug=False, routes=None, middleware=None, title: str='FastHTML page', exception_handlers=None, on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None, before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware, secret_key=None, session_cookie='session_', max_age=365 * 24 * 3600, sess_path='/', same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, **bodykw):
246
314
  ...
247
315
 
316
+ def on_event(self, event_type):
317
+ ...
318
+
248
319
  def add_route(self, route):
320
+ """Add or replace a route in the FastHTML app"""
249
321
  ...
250
322
 
251
- def _endp(self, f, body_wrap):
323
+ def _endp(self, f, body_wrap, before: Optional[Callable | tuple]=None):
324
+ """Create endpoint wrapper with before/after middleware processing"""
252
325
  ...
253
326
 
254
327
  def _add_ws(self, func, path, conn, disconn, name, middleware):
328
+ """Add websocket route to FastHTML app"""
255
329
  ...
256
330
 
257
331
  def ws(self, path: str, conn=None, disconn=None, name=None, middleware=None):
258
332
  """Add a websocket route at `path`"""
259
333
  ...
260
334
 
261
- def _add_route(self, func, path, methods, name, include_in_schema, body_wrap):
335
+ def add_websocket_route(self, path, func, conn=None, disconn=None, name=None, middleware=None):
336
+ """Add a websocket route at `path` (Starlette-compatible API)"""
337
+ ...
338
+
339
+ def _add_routes(self, cls, path, methods, name, include_in_schema, body_wrap, host=None, before: Optional[Callable | tuple]=None):
340
+ """Add HTTP routes from methods on endpoint class `cls`"""
262
341
  ...
263
342
 
264
- def route(self, path: str=None, methods=None, name=None, include_in_schema=True, body_wrap=None):
343
+ def _add_route(self, func, path, methods, name, include_in_schema, body_wrap, host=None, before: Optional[Callable | tuple]=None):
344
+ """Add HTTP route to FastHTML app with automatic method detection"""
345
+ ...
346
+
347
+ def route(self, path: str=None, methods=None, name=None, include_in_schema=True, body_wrap=None, host=None, before: Optional[Callable | tuple]=None):
265
348
  """Add a route at `path`"""
266
349
  ...
267
350
 
268
351
  def set_lifespan(self, value):
352
+ """Set the lifespan context manager for the FastHTML app"""
269
353
  ...
270
354
 
271
355
  def static_route_exts(self, prefix='/', static_path='.', exts='static'):
@@ -281,18 +365,47 @@ class FastHTML(Starlette):
281
365
 
282
366
  def devtools_json(self, path=None, uuid=None):
283
367
  ...
368
+
369
+ def get_client(self, asink=False, **kw):
370
+ """Get an httpx client with session cookes set from `**kw`"""
371
+ ...
372
+
373
+ def decode_session(self, cookie):
374
+ """Decode a signed session cookie"""
375
+ ...
376
+
377
+ def get_testclient(self, **kw):
378
+ """Get a Starlette `TestClient` with session cookies set from `**kw`"""
379
+ ...
380
+
381
+ class HostRoute(Route):
382
+ """Route with optional host-header constraint using Starlette's {param} pattern syntax"""
383
+
384
+ def __init__(self, path, endpoint, *, host=None, **kwargs):
385
+ ...
386
+
387
+ def matches(self, scope):
388
+ ...
389
+
390
+ def __repr__(self) -> str:
391
+ ...
284
392
  all_meths = 'get post put delete patch head trace options'.split()
285
393
 
286
- def _mk_locfunc(f, p):
394
+ def _mk_locfunc(f, p, app=None):
395
+ """Create a location function wrapper with route path and to() method"""
287
396
  ...
288
397
 
289
398
  def nested_name(f):
290
399
  """Get name of function `f` using '_' to join nested function names"""
291
400
  ...
401
+
402
+ def _route_pn(func, path, name):
403
+ """Infer route name/function name/path from an endpoint or endpoint class"""
404
+ ...
292
405
  for o in all_meths:
293
406
  setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
294
407
 
295
- def serve(appname=None, app='app', host='0.0.0.0', port=None, reload=True, reload_includes: list[str] | str | None=None, reload_excludes: list[str] | str | None=None):
408
+ def serve(appname=None, app='app', host='0.0.0.0', port=None, reload=True, **kwargs):
296
409
  """Run the app in an async server, with live reload set as the default."""
297
410
  ...
298
411
 
@@ -327,7 +440,7 @@ class APIRouter:
327
440
  def __init__(self, prefix: str | None=None, body_wrap=noop_body):
328
441
  ...
329
442
 
330
- def _wrap_func(self, func, path=None):
443
+ def _wrap_func(self, func, path=None, name=None):
331
444
  ...
332
445
 
333
446
  def __call__(self, path: str=None, methods=None, name=None, include_in_schema=True, body_wrap=None):
@@ -357,6 +470,26 @@ reg_re_param('path', '.*?')
357
470
  _static_exts = 'ico gif jpg jpeg webm css js woff png svg mp4 webp ttf otf eot woff2 txt html map pdf zip tgz gz csv mp3 wav ogg flac aac doc docx xls xlsx ppt pptx epub mobi bmp tiff avi mov wmv mkv xml yaml yml rar 7z tar bz2 htm xhtml apk dmg exe msi swf iso'.split()
358
471
  reg_re_param('static', '|'.join(_static_exts))
359
472
 
473
+ class StaticNoCache(StaticFiles):
474
+
475
+ def file_response(self, *args, **kwargs):
476
+ ...
477
+ from functools import wraps
478
+ from inspect import signature, isawaitable
479
+
480
+ def add_sig_param(f, name, typ=NoneType, kind=Parameter.KEYWORD_ONLY, default=Parameter.empty):
481
+ """Add a parameter to a function's signature"""
482
+ ...
483
+
484
+ class into:
485
+ """Decorator to pass a route's return value into `func`, with keyword params added to the route signature"""
486
+
487
+ def __init__(self, func):
488
+ ...
489
+
490
+ def __call__(self, f):
491
+ ...
492
+
360
493
  class MiddlewareBase:
361
494
 
362
495
  async def __call__(self, scope, receive, send) -> None:
@@ -1,12 +1,9 @@
1
1
  """The `fast_app` convenience wrapper"""
2
2
 
3
- import inspect,uvicorn
3
+ import inspect
4
4
  from fastcore.utils import *
5
- from fastlite import *
6
5
  from .basics import *
7
- from .pico import *
8
6
  from .starlette import *
9
- from .live_reload import FastHTMLWithLiveReload
10
7
 
11
8
  __all__ = ['fast_app']
12
9
 
@@ -19,8 +16,9 @@ def _get_tbl(dt, nm, schema):
19
16
  if render: dc.__ft__ = render
20
17
  return tbl,dc
21
18
 
22
- def _app_factory(*args, **kwargs) -> FastHTML | FastHTMLWithLiveReload:
19
+ def _app_factory(*args, **kwargs):
23
20
  "Creates a FastHTML or FastHTMLWithLiveReload app instance"
21
+ from .live_reload import FastHTMLWithLiveReload
24
22
  if kwargs.pop('live', False): return FastHTMLWithLiveReload(*args, **kwargs)
25
23
  kwargs.pop('reload_attempts', None)
26
24
  kwargs.pop('reload_interval', None)
@@ -65,6 +63,7 @@ def fast_app(
65
63
  nb_hdrs:bool=False, # If in notebook include headers inject headers in notebook DOM?
66
64
  **kwargs):
67
65
  "Create a FastHTML or FastHTMLWithLiveReload app."
66
+ from .pico import picolink
68
67
  h = (picolink,) if pico or (pico is None and default_hdrs) else ()
69
68
  if hdrs: h += tuple(hdrs)
70
69
 
@@ -76,6 +75,7 @@ def fast_app(
76
75
  app.static_route_exts(static_path=static_path)
77
76
  if not db_file: return app,app.route
78
77
 
78
+ from fastlite import database
79
79
  db = database(db_file)
80
80
  if not tbls: tbls={}
81
81
  if kwargs:
@@ -18,12 +18,11 @@ try: from IPython.display import HTML,Markdown,display
18
18
  except ImportError: pass
19
19
 
20
20
  # %% ../nbs/api/06_jupyter.ipynb #a5d3a8f7
21
- def nb_serve(app, log_level="error", port=8000, host='0.0.0.0', **kwargs):
22
- "Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
21
+ def nb_serve(app, log_level="error", port=8000, host='0.0.0.0', daemon=False, **kwargs):
22
+ "Start a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`; use `daemon=True` for notebook tests so failed runs don't block process shutdown"
23
23
  server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, host=host, port=port, **kwargs))
24
- async def async_run_server(server): await server.serve()
25
- @startthread
26
- def run_server(): asyncio.run(async_run_server(server))
24
+ @startthread(daemon=daemon)
25
+ def run_server(): asyncio.run(server.serve())
27
26
  while not server.started: time.sleep(0.01)
28
27
  return server
29
28
 
@@ -83,7 +82,7 @@ document.body.addEventListener('htmx:configRequest', (event) => {
83
82
  # %% ../nbs/api/06_jupyter.ipynb #29a834a5
84
83
  class JupyUvi:
85
84
  "Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
86
- def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True, **kwargs):
85
+ def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True, daemon=False, **kwargs):
87
86
  self.kwargs = kwargs
88
87
  store_attr(but='start')
89
88
  self.server = None
@@ -91,7 +90,7 @@ class JupyUvi:
91
90
  if not os.environ.get('IN_SOLVEIT'): htmx_config_port(port)
92
91
 
93
92
  def start(self):
94
- self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)
93
+ self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs)
95
94
 
96
95
  async def start_async(self):
97
96
  self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)
@@ -100,6 +99,7 @@ class JupyUvi:
100
99
  self.server.should_exit = True
101
100
  wait_port_free(self.port)
102
101
 
102
+
103
103
  # %% ../nbs/api/06_jupyter.ipynb #9134035e
104
104
  class JupyUviAsync(JupyUvi):
105
105
  "Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
@@ -20,9 +20,6 @@ from fastcore.meta import use_kwargs, delegates
20
20
  from .core import *
21
21
  from .components import *
22
22
 
23
- try: from IPython import display
24
- except ImportError: display=None
25
-
26
23
  # %% ../nbs/api/02_xtend.ipynb #254f4744
27
24
  @delegates(ft_hx, keep=True)
28
25
  def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', **kwargs)->FT:
@@ -1,5 +1,5 @@
1
1
  """Simple extensions to standard HTML components, such as adding sensible defaults"""
2
- __all__ = ['sid_scr', 'A', 'AX', 'Form', 'Hidden', 'CheckboxX', 'Script', 'Style', 'double_braces', 'undouble_braces', 'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'Nbsp', 'Surreal', 'On', 'Prev', 'Now', 'AnyNow', 'run_js', 'HtmxOn', 'jsd', 'Fragment', 'Titled', 'Socials', 'YouTubeEmbed', 'Favicon', 'clear', 'with_sid']
2
+ __all__ = ['sid_scr', 'A', 'AX', 'Form', 'Hidden', 'CheckboxX', 'Script', 'Style', 'double_braces', 'undouble_braces', 'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'Nbsp', 'Surreal', 'On', 'Prev', 'Now', 'AnyNow', 'run_js', 'HtmxOn', 'jsd', 'Fragment', 'Titled', 'Socials', 'YouTubeEmbed', 'Favicon', 'clear', 'with_sid', 'LdJson', 'LdContactPoint', 'LdOrg', 'LdWebsite', 'LdCourseInstance', 'LdCourse', 'robots_txt', 'sitemap_url', 'sitemap_xml']
3
3
  from dataclasses import dataclass, asdict
4
4
  from typing import Any
5
5
  from fastcore.utils import *
@@ -8,10 +8,6 @@ from fastcore.xml import *
8
8
  from fastcore.meta import use_kwargs, delegates
9
9
  from .core import *
10
10
  from .components import *
11
- try:
12
- from IPython import display
13
- except ImportError:
14
- display = None
15
11
 
16
12
  def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', hx_vals=None, hx_target=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_post=None, hx_put=None, hx_delete=None, hx_patch=None, hx_trigger=None, hx_swap_oob=None, hx_include=None, hx_select=None, hx_select_oob=None, hx_indicator=None, hx_push_url=None, hx_confirm=None, hx_disable=None, hx_replace_url=None, hx_disabled_elt=None, hx_ext=None, hx_headers=None, hx_history=None, hx_history_elt=None, hx_inherit=None, hx_params=None, hx_preserve=None, hx_prompt=None, hx_request=None, hx_sync=None, hx_validate=None, hx_on_blur=None, hx_on_change=None, hx_on_contextmenu=None, hx_on_focus=None, hx_on_input=None, hx_on_invalid=None, hx_on_reset=None, hx_on_select=None, hx_on_submit=None, hx_on_keydown=None, hx_on_keypress=None, hx_on_keyup=None, hx_on_click=None, hx_on_dblclick=None, hx_on_mousedown=None, hx_on_mouseenter=None, hx_on_mouseleave=None, hx_on_mousemove=None, hx_on_mouseout=None, hx_on_mouseover=None, hx_on_mouseup=None, hx_on_wheel=None, hx_on__abort=None, hx_on__after_on_load=None, hx_on__after_process_node=None, hx_on__after_request=None, hx_on__after_settle=None, hx_on__after_swap=None, hx_on__before_cleanup_element=None, hx_on__before_on_load=None, hx_on__before_process_node=None, hx_on__before_request=None, hx_on__before_swap=None, hx_on__before_send=None, hx_on__before_transition=None, hx_on__config_request=None, hx_on__confirm=None, hx_on__history_cache_error=None, hx_on__history_cache_miss=None, hx_on__history_cache_miss_error=None, hx_on__history_cache_miss_load=None, hx_on__history_restore=None, hx_on__before_history_save=None, hx_on__load=None, hx_on__no_sse_source_error=None, hx_on__on_load_error=None, hx_on__oob_after_swap=None, hx_on__oob_before_swap=None, hx_on__oob_error_no_target=None, hx_on__prompt=None, hx_on__pushed_into_history=None, hx_on__replaced_in_history=None, hx_on__response_error=None, hx_on__send_abort=None, hx_on__send_error=None, hx_on__sse_error=None, hx_on__sse_open=None, hx_on__swap_error=None, hx_on__target_error=None, hx_on__timeout=None, hx_on__validation_validate=None, hx_on__validation_failed=None, hx_on__validation_halted=None, hx_on__xhr_abort=None, hx_on__xhr_loadend=None, hx_on__xhr_loadstart=None, hx_on__xhr_progress=None, **kwargs) -> FT:
17
13
  """An A tag; `href` defaults to '#' for more concise use with HTMX"""
@@ -127,4 +123,41 @@ def clear(id):
127
123
  sid_scr = Script('\nfunction uuid() {\n return [...crypto.getRandomValues(new Uint8Array(10))].map(b=>b.toString(36)).join(\'\');\n}\n\nsessionStorage.setItem("sid", sessionStorage.getItem("sid") || uuid());\n\nhtmx.on("htmx:configRequest", (e) => {\n const sid = sessionStorage.getItem("sid");\n if (sid) {\n const url = new URL(e.detail.path, window.location.origin);\n url.searchParams.set(\'sid\', sid);\n e.detail.path = url.pathname + url.search;\n }\n});\n')
128
124
 
129
125
  def with_sid(app, dest, path='/'):
126
+ ...
127
+
128
+ def LdJson(typ, data: dict, script=False, extra=None, **kwargs) -> FT:
129
+ """A script tag containing JSON-LD structured data"""
130
+ ...
131
+
132
+ def LdContactPoint(contact_type: str, email: str=None, phone: str=None, script=False, **extra) -> dict:
133
+ """Create a ContactPoint for JSON-LD"""
134
+ ...
135
+
136
+ def LdOrg(name: str, url: str=None, logo: str=None, alt_name: str=None, same_as: list=None, contact_points: list=None, script=False, **extra) -> dict:
137
+ """JSON-LD Organization structured data"""
138
+ ...
139
+
140
+ def LdWebsite(name: str, url: str, script=False, **extra) -> dict:
141
+ """Create JSON-LD WebSite structured data"""
142
+ ...
143
+
144
+ def LdCourseInstance(course_mode: str='Online', start_date: str=None, end_date: str=None, location: dict=None, instructor: dict=None, script=False, **extra) -> dict:
145
+ """Create a CourseInstance for JSON-LD"""
146
+ ...
147
+
148
+ def LdCourse(name: str, description: str, provider: dict, course_instance: dict=None, script=False, **extra) -> dict:
149
+ """Create JSON-LD Course structured data"""
150
+ ...
151
+
152
+ def robots_txt(app, allow_all=True, disallow_paths=None, sitemap_url=None, crawl_delay=None):
153
+ """Add a /robots.txt route to the app"""
154
+ ...
155
+ from fastcore.xml import Url, Loc, Lastmod, Changefreq, Priority, Urlset
156
+
157
+ def sitemap_url(url_info, loc_base=''):
158
+ """Create a sitemap URL element from url_info (string or dict)"""
159
+ ...
160
+
161
+ def sitemap_xml(app, urls, loc_base=''):
162
+ """Add a /sitemap.xml route to the app with list of URLs"""
130
163
  ...
@@ -12,7 +12,7 @@ license = {text = "Apache-2.0"}
12
12
  authors = [{name = "Jeremy Howard and contributors", email = "github@jhoward.fastmail.fm"}]
13
13
  keywords = ['nbdev', 'jupyter', 'notebook', 'python']
14
14
  classifiers = ["Natural Language :: English", "Intended Audience :: Developers", "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
15
- dependencies = ['fastcore>=1.12.16', 'python-dateutil', 'starlette~=1.0', 'oauthlib', 'itsdangerous', 'uvicorn[standard]>=0.30', 'httpx', 'fastlite>=0.1.1', 'python-multipart', 'beautifulsoup4']
15
+ dependencies = ['fastcore>=1.12.45', 'python-dateutil', 'starlette~=1.0', 'oauthlib', 'itsdangerous', 'uvicorn[standard]>=0.30', 'httpx', 'fastlite>=0.1.1', 'python-multipart', 'beautifulsoup4']
16
16
 
17
17
  [project.urls]
18
18
  Repository = "https://github.com/AnswerDotAI/fasthtml"
@@ -36,8 +36,8 @@ version = {attr = "fasthtml.__version__"}
36
36
  include = ["fasthtml"]
37
37
 
38
38
  [tool.nbdev]
39
- allowed_metadata_keys = ['solveit_dialog_mode', 'solveit_ver']
40
- allowed_cell_metadata_keys = ["solveit_ai"]
39
+ allowed_metadata_keys = ['solveit']
40
+ allowed_cell_metadata_keys = ['solveit_ai']
41
41
  jupyter_hooks = true
42
42
  custom_sidebar = false
43
43
  lib_path = "fasthtml"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fasthtml
3
- Version: 0.13.3
3
+ Version: 0.14.0
4
4
  Summary: The fastest way to create an HTML app
5
5
  Author-email: Jeremy Howard and contributors <github@jhoward.fastmail.fm>
6
6
  License: Apache-2.0
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: fastcore>=1.12.16
18
+ Requires-Dist: fastcore>=1.12.45
19
19
  Requires-Dist: python-dateutil
20
20
  Requires-Dist: starlette~=1.0
21
21
  Requires-Dist: oauthlib
@@ -1,4 +1,4 @@
1
- fastcore>=1.12.16
1
+ fastcore>=1.12.45
2
2
  python-dateutil
3
3
  starlette~=1.0
4
4
  oauthlib
@@ -1,2 +0,0 @@
1
- __version__ = "0.13.3"
2
- from .core import *