python-fasthtml 0.13.4__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.4/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.4 → python_fasthtml-0.14.0}/fasthtml/_modidx.py +4 -0
  4. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/common.py +0 -4
  5. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/components.py +2 -4
  6. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/components.pyi +0 -5
  7. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/core.py +69 -19
  8. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/core.pyi +166 -33
  9. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/fastapp.py +5 -5
  10. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/jupyter.py +7 -7
  11. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/xtend.py +0 -3
  12. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/xtend.pyi +38 -5
  13. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/pyproject.toml +3 -3
  14. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0/python_fasthtml.egg-info}/PKG-INFO +2 -2
  15. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/requires.txt +1 -1
  16. python_fasthtml-0.13.4/fasthtml/__init__.py +0 -2
  17. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/CONTRIBUTING.md +0 -0
  18. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/LICENSE +0 -0
  19. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/MANIFEST.in +0 -0
  20. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/README.md +0 -0
  21. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/authmw.py +0 -0
  22. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/basics.py +0 -0
  23. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/cli.py +0 -0
  24. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/ft.py +0 -0
  25. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/js.py +0 -0
  26. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/katex.js +0 -0
  27. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/live_reload.py +0 -0
  28. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/oauth.py +0 -0
  29. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/pico.py +0 -0
  30. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/starlette.py +0 -0
  31. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/stripe_otp.py +0 -0
  32. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/svg.py +0 -0
  33. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/fasthtml/toaster.py +0 -0
  34. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/SOURCES.txt +0 -0
  35. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  36. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/entry_points.txt +0 -0
  37. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/top_level.txt +0 -0
  38. {python_fasthtml-0.13.4 → python_fasthtml-0.14.0}/setup.cfg +0 -0
  39. {python_fasthtml-0.13.4 → 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.4
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.41
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'),
@@ -123,6 +126,7 @@ d = { 'settings': { 'branch': 'main',
123
126
  'fasthtml.core._params': ('api/core.html#_params', 'fasthtml/core.py'),
124
127
  'fasthtml.core._part_resp': ('api/core.html#_part_resp', 'fasthtml/core.py'),
125
128
  'fasthtml.core._resp': ('api/core.html#_resp', 'fasthtml/core.py'),
129
+ 'fasthtml.core._route_pn': ('api/core.html#_route_pn', 'fasthtml/core.py'),
126
130
  'fasthtml.core._send_ws': ('api/core.html#_send_ws', 'fasthtml/core.py'),
127
131
  'fasthtml.core._to_htmx_header': ('api/core.html#_to_htmx_header', 'fasthtml/core.py'),
128
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'}
@@ -664,7 +666,7 @@ all_meths = 'get post put delete patch head trace options'.split()
664
666
 
665
667
  # %% ../nbs/api/00_core.ipynb #26b147ba
666
668
  @patch
667
- def _endp(self:FastHTML, f, body_wrap):
669
+ def _endp(self:FastHTML, f, body_wrap, before:Optional[Callable|tuple]=None):
668
670
  "Create endpoint wrapper with before/after middleware processing"
669
671
  sig = signature_ex(f, True)
670
672
  for n,p in sig.parameters.items(): (msg:=_check_anno(n,p.annotation)) and warn(msg)
@@ -679,6 +681,8 @@ def _endp(self:FastHTML, f, body_wrap):
679
681
  else: bf,skip = b,[]
680
682
  if not any(re.fullmatch(r, req.url.path) for r in skip):
681
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))
682
686
  req.body_wrap = body_wrap
683
687
  if not resp: resp = await _wrap_call(f, req, sig.parameters)
684
688
  for a in self.after:
@@ -731,17 +735,37 @@ def nested_name(f):
731
735
  "Get name of function `f` using '_' to join nested function names"
732
736
  return f.__qualname__.replace('.<locals>.', '_')
733
737
 
734
- # %% ../nbs/api/00_core.ipynb #72760b09
738
+ # %% ../nbs/api/00_core.ipynb #daafe4fc
735
739
  @patch
736
- def _add_route(self:FastHTML, func, path, methods, name, include_in_schema, body_wrap, host=None):
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
+
760
+ @patch
761
+ def _add_route(self:FastHTML, func, path, methods, name, include_in_schema, body_wrap, host=None, before:Optional[Callable|tuple]=None):
737
762
  "Add HTTP route to FastHTML app with automatic method detection"
738
- 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)
739
765
  if methods: m = [methods] if isinstance(methods,str) else methods
740
766
  elif fn in all_meths and p is not None: m = [fn]
741
767
  else: m = ['get','post']
742
- if not n: n = fn
743
- if not p: p = '/'+('' if fn=='index' else fn)
744
- endp = self._endp(func, body_wrap or self.body_wrap)
768
+ endp = self._endp(func, body_wrap or self.body_wrap, before=before)
745
769
  route = HostRoute(p, endpoint=endp, methods=m, name=n, include_in_schema=include_in_schema, host=host)
746
770
  self.add_route(route)
747
771
  lf = _mk_locfunc(func, p, app=self)
@@ -750,10 +774,10 @@ def _add_route(self:FastHTML, func, path, methods, name, include_in_schema, body
750
774
 
751
775
  # %% ../nbs/api/00_core.ipynb #f5cb2c2b
752
776
  @patch
753
- 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):
754
778
  "Add a route at `path`"
755
779
  def f(func):
756
- 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)
757
781
  return f(path) if callable(path) else f
758
782
 
759
783
  for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
@@ -766,7 +790,6 @@ def set_lifespan(self:FastHTML, value):
766
790
  self.router.lifespan_context = value
767
791
 
768
792
  # %% ../nbs/api/00_core.ipynb #3a348474
769
- @delegates(uvicorn.run)
770
793
  def serve(
771
794
  appname=None, # Name of the module
772
795
  app='app', # App instance to be served
@@ -776,6 +799,7 @@ def serve(
776
799
  **kwargs
777
800
  ):
778
801
  "Run the app in an async server, with live reload set as the default."
802
+ from uvicorn import run
779
803
  bk = inspect.currentframe().f_back
780
804
  glb = bk.f_globals
781
805
  code = bk.f_code
@@ -792,6 +816,7 @@ def serve(
792
816
  class Client:
793
817
  "A simple httpx ASGI client that doesn't require `async`"
794
818
  def __init__(self, app, url="http://testserver"):
819
+ import httpx
795
820
  self.cli = httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url=url)
796
821
 
797
822
  def _sync(self, method, url, **kwargs):
@@ -819,20 +844,20 @@ class APIRouter:
819
844
  self.prefix = prefix if prefix else ""
820
845
  self.body_wrap = body_wrap
821
846
 
822
- def _wrap_func(self, func, path=None):
823
- name = func.__name__
847
+ def _wrap_func(self, func, path=None, name=None):
824
848
  wrapped = _mk_locfunc(func, path)
825
849
  wrapped.__routename__ = name
826
- # 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
827
851
  if name not in all_meths: setattr(self.rt_funcs, name, wrapped)
828
852
  return wrapped
829
853
 
830
854
  def __call__(self, path:str=None, methods=None, name=None, include_in_schema=True, body_wrap=None):
831
855
  "Add a route at `path`"
832
856
  def f(func):
833
- p = self.prefix + ("/" + ('' if path.__name__=='index' else func.__name__) if callable(path) else path)
834
- wrapped = self._wrap_func(func, p)
835
- 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))
836
861
  return wrapped
837
862
  return f(path) if callable(path) else f
838
863
 
@@ -1001,9 +1026,34 @@ def devtools_json(self:FastHTML, path=None, uuid=None):
1001
1026
  @patch
1002
1027
  def get_client(self:FastHTML, asink=False, **kw):
1003
1028
  "Get an httpx client with session cookes set from `**kw`"
1029
+ import httpx
1004
1030
  signer = itsdangerous.TimestampSigner(self.secret_key)
1005
1031
  data = b64encode(dumps(kw).encode())
1006
1032
  data = signer.sign(data)
1007
1033
  client = httpx.AsyncClient() if asink else httpx.Client()
1008
1034
  client.cookies.update({self.session_cookie: data.decode()})
1009
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.41', '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.4
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.41
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.41
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.4"
2
- from .core import *