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.
- {python_fasthtml-0.13.3/python_fasthtml.egg-info → python_fasthtml-0.14.0}/PKG-INFO +2 -2
- python_fasthtml-0.14.0/fasthtml/__init__.py +2 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/_modidx.py +4 -1
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/common.py +0 -4
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/components.py +2 -4
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/components.pyi +0 -5
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/core.py +74 -30
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/core.pyi +166 -33
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/fastapp.py +5 -5
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/jupyter.py +7 -7
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/xtend.py +0 -3
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/xtend.pyi +38 -5
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/pyproject.toml +3 -3
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0/python_fasthtml.egg-info}/PKG-INFO +2 -2
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/requires.txt +1 -1
- python_fasthtml-0.13.3/fasthtml/__init__.py +0 -2
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/CONTRIBUTING.md +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/LICENSE +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/MANIFEST.in +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/README.md +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/basics.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/cli.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/ft.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/js.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/katex.js +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/oauth.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/pico.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/stripe_otp.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/svg.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/SOURCES.txt +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/setup.cfg +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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,
|
|
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:
|
|
191
|
-
|
|
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
|
|
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 #
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
840
|
-
|
|
841
|
-
self.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
11
|
-
from
|
|
12
|
-
from
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
110
|
-
"""In `
|
|
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
|
-
|
|
126
|
-
|
|
149
|
+
def __repr__(self):
|
|
150
|
+
...
|
|
127
151
|
|
|
128
|
-
def
|
|
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-
|
|
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
|
|
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.
|
|
217
|
-
htmxsrc = Script(src='https://cdn.jsdelivr.net/npm/htmx.org@2.0.
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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)
|
|
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
|
-
|
|
25
|
-
|
|
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.
|
|
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 = ['
|
|
40
|
-
allowed_cell_metadata_keys = [
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fasthtml-0.13.3 → python_fasthtml-0.14.0}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|