python-fasthtml 0.14.11__tar.gz → 0.14.12__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.14.11/python_fasthtml.egg-info → python_fasthtml-0.14.12}/PKG-INFO +2 -3
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/README.md +0 -1
- python_fasthtml-0.14.12/fasthtml/__init__.py +8 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/components.py +23 -14
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/core.py +107 -25
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/fastapp.py +2 -2
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/jupyter.py +10 -6
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/pico.py +7 -7
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/xtend.py +16 -14
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/pyproject.toml +1 -2
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12/python_fasthtml.egg-info}/PKG-INFO +2 -3
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/requires.txt +1 -1
- python_fasthtml-0.14.11/fasthtml/__init__.py +0 -2
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/CONTRIBUTING.md +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/LICENSE +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/MANIFEST.in +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/_modidx.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/basics.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/cli.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/common.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/components.pyi +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/core.pyi +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/ft.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/js.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/katex.js +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/oauth.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/stripe_otp.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/svg.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/fasthtml/xtend.pyi +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/SOURCES.txt +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/setup.cfg +0 -0
- {python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/tests/test_toaster.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.12
|
|
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>=2.
|
|
18
|
+
Requires-Dist: fastcore>=2.2.7
|
|
19
19
|
Requires-Dist: python-dateutil
|
|
20
20
|
Requires-Dist: starlette>=1.0.1
|
|
21
21
|
Requires-Dist: oauthlib
|
|
@@ -128,7 +128,6 @@ The capabilities of FastHTML are vast and growing, and not all the features and
|
|
|
128
128
|
Then explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:
|
|
129
129
|
|
|
130
130
|
- [FastHTML Gallery](https://gallery.fastht.ml): Learn from minimal examples of components (ie chat bubbles, click-to-edit, infinite scroll, etc)
|
|
131
|
-
- [Creating Custom FastHTML Tags for Markdown Rendering](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html) by Isaac Flath
|
|
132
131
|
- [How to Build a Simple Login System in FastHTML](https://blog.mariusvach.com/posts/login-fasthtml) by Marius Vach
|
|
133
132
|
- Your tutorial here!
|
|
134
133
|
|
|
@@ -93,7 +93,6 @@ The capabilities of FastHTML are vast and growing, and not all the features and
|
|
|
93
93
|
Then explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:
|
|
94
94
|
|
|
95
95
|
- [FastHTML Gallery](https://gallery.fastht.ml): Learn from minimal examples of components (ie chat bubbles, click-to-edit, infinite scroll, etc)
|
|
96
|
-
- [Creating Custom FastHTML Tags for Markdown Rendering](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html) by Isaac Flath
|
|
97
96
|
- [How to Build a Simple Login System in FastHTML](https://blog.mariusvach.com/posts/login-fasthtml) by Marius Vach
|
|
98
97
|
- Your tutorial here!
|
|
99
98
|
|
|
@@ -5,18 +5,18 @@ Docs: https://www.fastht.ml/docs/api/components.html.md"""
|
|
|
5
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/01_components.ipynb.
|
|
6
6
|
|
|
7
7
|
# %% auto #0
|
|
8
|
-
__all__ = ['named', 'html_attrs', 'hx_attrs', '
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'Var', 'Video', 'Wbr']
|
|
8
|
+
__all__ = ['named', 'html_attrs', 'hx_attrs', 'hx_attrs_4only', 'hx_evts', 'hx_evts_4only', 'js_evts', 'hx_attrs_annotations',
|
|
9
|
+
'hx_evt_attrs', 'js_evt_attrs', 'evt_attrs', 'HxPartial', 'attrmap_x', 'ft_html', 'ft_hx', 'File', 'show',
|
|
10
|
+
'fill_form', 'fill_dataclass', 'find_inputs', 'html2ft', 'sse_message', 'A', 'Abbr', 'Address', 'Area',
|
|
11
|
+
'Article', 'Aside', 'Audio', 'B', 'Base', 'Bdi', 'Bdo', 'Blockquote', 'Body', 'Br', 'Button', 'Canvas',
|
|
12
|
+
'Caption', 'Cite', 'Code', 'Col', 'Colgroup', 'Data', 'Datalist', 'Dd', 'Del', 'Details', 'Dfn', 'Dialog',
|
|
13
|
+
'Div', 'Dl', 'Dt', 'Em', 'Embed', 'Fencedframe', 'Fieldset', 'Figcaption', 'Figure', 'Footer', 'Form', 'H1',
|
|
14
|
+
'H2', 'H3', 'H4', 'H5', 'H6', 'Head', 'Header', 'Hgroup', 'Hr', 'I', 'Iframe', 'Img', 'Input', 'Ins', 'Kbd',
|
|
15
|
+
'Label', 'Legend', 'Li', 'Link', 'Main', 'Map', 'Mark', 'Menu', 'Meta', 'Meter', 'Nav', 'Noscript', 'Object',
|
|
16
|
+
'Ol', 'Optgroup', 'Option', 'Output', 'P', 'Picture', 'PortalExperimental', 'Pre', 'Progress', 'Q', 'Rp',
|
|
17
|
+
'Rt', 'Ruby', 'S', 'Samp', 'Script', 'Search', 'Section', 'Select', 'Slot', 'Small', 'Source', 'Span',
|
|
18
|
+
'Strong', 'Style', 'Sub', 'Summary', 'Sup', 'Table', 'Tbody', 'Td', 'Template', 'Textarea', 'Tfoot', 'Th',
|
|
19
|
+
'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']
|
|
20
20
|
|
|
21
21
|
# %% ../nbs/api/01_components.ipynb #8e2d405b
|
|
22
22
|
from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING
|
|
@@ -46,10 +46,13 @@ def __add__(self:FT, b): return f'{self}{b}'
|
|
|
46
46
|
named = set('a button form frame iframe img input map meta object param select textarea'.split())
|
|
47
47
|
html_attrs = 'id cls title style accesskey contenteditable dir draggable enterkeyhint hidden inert inputmode lang popover spellcheck tabindex translate'.split()
|
|
48
48
|
hx_attrs = 'get post put delete patch trigger target swap swap_oob include select select_oob indicator push_url confirm disable replace_url vals disabled_elt ext headers history history_elt inherit params preserve prompt request sync validate'
|
|
49
|
+
hx_attrs_4only = 'action method config ignore' # https://four.htmx.org/docs/get-started/migration#attributes
|
|
49
50
|
|
|
50
51
|
hx_evts = 'abort afterOnLoad afterProcessNode afterRequest afterSettle afterSwap beforeCleanupElement beforeOnLoad beforeProcessNode beforeRequest beforeSwap beforeSend beforeTransition configRequest confirm historyCacheError historyCacheMiss historyCacheMissError historyCacheMissLoad historyRestore beforeHistorySave load noSSESourceError onLoadError oobAfterSwap oobBeforeSwap oobErrorNoTarget prompt pushedIntoHistory replacedInHistory responseError sendAbort sendError sseError sseOpen swapError targetError timeout validation:validate validation:failed validation:halted xhr:abort xhr:loadend xhr:loadstart xhr:progress'
|
|
52
|
+
# https://four.htmx.org/docs/get-started/migration#renamed-events
|
|
53
|
+
hx_evts_4only = 'after:init before:cleanup before:history:update before:init before:process before:history:restore after:history:push after:history:replace error'
|
|
51
54
|
js_evts = "blur change contextmenu focus input invalid reset select submit keydown keypress keyup click dblclick mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup wheel"
|
|
52
|
-
hx_attrs = [f'hx_{o}' for o in hx_attrs.split()]
|
|
55
|
+
hx_attrs = [f'hx_{o}' for o in hx_attrs.split() + hx_attrs_4only.split()]
|
|
53
56
|
hx_attrs_annotations = {
|
|
54
57
|
"hx_swap": Literal["innerHTML", "outerHTML", "afterbegin", "beforebegin", "beforeend", "afterend", "delete", "none"] | str,
|
|
55
58
|
"hx_swap_oob": Literal["true", "innerHTML", "outerHTML", "afterbegin", "beforebegin", "beforeend", "afterend", "delete", "none"] | str,
|
|
@@ -64,7 +67,7 @@ hx_attrs_annotations |= {o: str for o in set(hx_attrs) - set(hx_attrs_annotation
|
|
|
64
67
|
hx_attrs_annotations = {k: Optional[v] for k,v in hx_attrs_annotations.items()}
|
|
65
68
|
hx_attrs = html_attrs + hx_attrs
|
|
66
69
|
|
|
67
|
-
hx_evt_attrs = ['hx_on__'+camel2snake(o).replace(':','_') for o in hx_evts.split()]
|
|
70
|
+
hx_evt_attrs = ['hx_on__'+camel2snake(o).replace(':','_') for o in hx_evts.split() + hx_evts_4only.split()]
|
|
68
71
|
js_evt_attrs = ['hx_on_'+o for o in js_evts.split()]
|
|
69
72
|
evt_attrs = js_evt_attrs+hx_evt_attrs
|
|
70
73
|
|
|
@@ -117,6 +120,10 @@ _all_ = [
|
|
|
117
120
|
'Td', 'Template', 'Textarea', 'Tfoot', 'Th', 'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']
|
|
118
121
|
for o in _all_: _g[o] = partial(ft_hx, o.lower())
|
|
119
122
|
|
|
123
|
+
# %% ../nbs/api/01_components.ipynb #dfa8f01c
|
|
124
|
+
HxPartial = partial(ft_hx, 'hx-partial') # For hx v4, manually mapping because Python doesn't allow hyphens
|
|
125
|
+
|
|
126
|
+
|
|
120
127
|
# %% ../nbs/api/01_components.ipynb #fab04fb3
|
|
121
128
|
def File(fname):
|
|
122
129
|
"Use the unescaped text in file `fname` directly"
|
|
@@ -243,4 +250,6 @@ def html2ft(html, attr1st=False):
|
|
|
243
250
|
def sse_message(elm, event='message'):
|
|
244
251
|
"Convert element `elm` into a format suitable for SSE streaming"
|
|
245
252
|
data = '\n'.join(f'data: {o}' for o in to_xml(elm).splitlines())
|
|
253
|
+
# SSE delivers a message with no event field as type 'message', so the line is redundant for the default (and htmx v4 requires its absence for plain swaps)
|
|
254
|
+
if event=='message': return f'{data}\n\n'
|
|
246
255
|
return f'event: {event}\n{data}\n\n'
|
|
@@ -1,12 +1,52 @@
|
|
|
1
1
|
"""The `FastHTML` subclass of `Starlette`.
|
|
2
2
|
|
|
3
|
+
Create an app with `app = FastHTML()`. The `@rt` decorator adds routes (`rt = app.route`), inferring what it can from the function:
|
|
4
|
+
|
|
5
|
+
@rt
|
|
6
|
+
def index(): ... # GET,POST /
|
|
7
|
+
@rt
|
|
8
|
+
def foo(): ... # GET,POST /foo
|
|
9
|
+
@rt('/hi')
|
|
10
|
+
def get(): ... # GET /hi (function named after a verb handles just that verb)
|
|
11
|
+
@rt('/cards/{id}')
|
|
12
|
+
class Cards: ... # class route group: each verb-named method (get, patch, ...) becomes a handler
|
|
13
|
+
|
|
14
|
+
The decorated function doubles as a URL builder, so links never need hardcoded paths:
|
|
15
|
+
|
|
16
|
+
A('More', href=foo.to(a=1)) # <a href="/foo?a=1"> `.to()` fills path params; the rest become query params.
|
|
17
|
+
Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get="/foo"
|
|
18
|
+
|
|
19
|
+
Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in `<head>`; the rest goes in `<body>`. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.
|
|
20
|
+
|
|
21
|
+
Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation:
|
|
22
|
+
|
|
23
|
+
req, ws the Request / WebSocket connection
|
|
24
|
+
sess session dict
|
|
25
|
+
app the FastHTML app
|
|
26
|
+
state app.state
|
|
27
|
+
scope ASGI scope
|
|
28
|
+
auth scope['auth']
|
|
29
|
+
htmx HtmxHeaders (parsed HX-* request headers)
|
|
30
|
+
body raw body text
|
|
31
|
+
data parsed form data as a dict
|
|
32
|
+
api ApiReturn (API vs browser dual responses)
|
|
33
|
+
send websocket send function
|
|
34
|
+
hdrs,ftrs the per-request copies of the app's `hdrs`/`ftrs` (head and footer content)
|
|
35
|
+
htmlkw the per-request copy of the app's `<html>` attrs
|
|
36
|
+
bodykw the per-request copy of the app's `<body>` attrs
|
|
37
|
+
resp the handler's response (injected into `after` functions)
|
|
38
|
+
|
|
39
|
+
`before`/`after` functions (app-level, with `Beforeware` skip patterns, or per-route `before=`) get the same param injection as handlers. A `before` returning a value responds with it and skips the handler (the usual auth-guard pattern).
|
|
40
|
+
|
|
41
|
+
See the [Handling Handlers tutorial](https://www.fastht.ml/docs/ref/handlers.html.md) for worked examples of the above.
|
|
42
|
+
|
|
3
43
|
Docs: https://www.fastht.ml/docs/api/core.html.md"""
|
|
4
44
|
|
|
5
45
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.
|
|
6
46
|
|
|
7
47
|
# %% auto #0
|
|
8
|
-
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'DEF_MAXPART', 'htmx_exts', 'htmxsrc', '
|
|
9
|
-
'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
|
|
48
|
+
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'DEF_MAXPART', 'htmx_exts', 'htmxsrc', 'htmx4src', 'fhjsscr', 'surrsrc',
|
|
49
|
+
'scopesrc', 'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
|
|
10
50
|
'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn',
|
|
11
51
|
'JSONResponse', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple',
|
|
12
52
|
'noop_body', 'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML',
|
|
@@ -64,14 +104,18 @@ htmx_hdrs = dict(
|
|
|
64
104
|
history_restore_request="HX-History-Restore-Request",
|
|
65
105
|
prompt="HX-Prompt",
|
|
66
106
|
request="HX-Request",
|
|
107
|
+
request_type="HX-Request-Type", # v4: partial or full update
|
|
108
|
+
source="HX-Source", # v4: replaces v2 HX-Trigger but with tag#id instead just id
|
|
67
109
|
target="HX-Target",
|
|
68
110
|
trigger_name="HX-Trigger-Name",
|
|
69
111
|
trigger="HX-Trigger")
|
|
70
112
|
|
|
71
113
|
@dataclass
|
|
72
114
|
class HtmxHeaders:
|
|
115
|
+
"Parsed HX-* request headers"
|
|
73
116
|
boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None
|
|
74
|
-
request:str|None=None;
|
|
117
|
+
request:str|None=None; request_type:str|None=None; source:str|None=None; target:str|None=None;
|
|
118
|
+
trigger_name:str|None=None; trigger:str|None=None
|
|
75
119
|
def __bool__(self): return any(hasattr(self,o) for o in htmx_hdrs)
|
|
76
120
|
|
|
77
121
|
def _get_htmx(h):
|
|
@@ -117,7 +161,9 @@ def _form_arg(k, v, d):
|
|
|
117
161
|
|
|
118
162
|
# %% ../nbs/api/00_core.ipynb #5fc04751
|
|
119
163
|
@dataclass
|
|
120
|
-
class HttpHeader:
|
|
164
|
+
class HttpHeader:
|
|
165
|
+
"A response header as a k,v pair"
|
|
166
|
+
k:str;v:str
|
|
121
167
|
|
|
122
168
|
# %% ../nbs/api/00_core.ipynb #94e18161
|
|
123
169
|
def _to_htmx_header(s): return 'HX-' + s.replace('_', '-').title()
|
|
@@ -188,6 +234,7 @@ async def _from_body(conn, p, data):
|
|
|
188
234
|
|
|
189
235
|
# %% ../nbs/api/00_core.ipynb #88b6da3f
|
|
190
236
|
class ApiReturn:
|
|
237
|
+
"Request param: call with browser and API responses; returns kwargs as JSON if the client accepts `application/json`, else `norm`"
|
|
191
238
|
@classmethod
|
|
192
239
|
async def __from_request__(cls, data, req): return cls(req.headers.get('accept')=='application/json')
|
|
193
240
|
def __init__(self, isapi=False): self.isapi = isapi
|
|
@@ -242,6 +289,7 @@ async def _find_p(conn, data, hdrs, arg:str, p:Parameter):
|
|
|
242
289
|
if res in (empty,None): res = conn.query_params.getlist(arg)
|
|
243
290
|
if res==[]: res = None
|
|
244
291
|
if res in (empty,None): res = data.get(arg, None)
|
|
292
|
+
if res in (empty,None) and isinstance(vals := data.get('values'), dict): res = vals.get(arg) # htmx v4 nests form data under `values`
|
|
245
293
|
if res in (empty,None):
|
|
246
294
|
if p.default is empty:
|
|
247
295
|
if isinstance(conn, Request): raise HTTPException(400, f"Missing required field: {arg}")
|
|
@@ -274,16 +322,18 @@ def flat_xt(lst):
|
|
|
274
322
|
|
|
275
323
|
# %% ../nbs/api/00_core.ipynb #aacff5ac
|
|
276
324
|
class Beforeware:
|
|
325
|
+
"Wrap a `before` function with `skip` path patterns that bypass it"
|
|
277
326
|
def __init__(self, f, skip=None): self.f,self.skip = f,skip or []
|
|
278
327
|
def __repr__(self): return f'Beforeware({self.f}, skip={self.skip})'
|
|
279
328
|
|
|
280
329
|
# %% ../nbs/api/00_core.ipynb #78c3c357
|
|
281
|
-
async def _handle(f, *args, **kwargs):
|
|
330
|
+
async def _handle(f, /, *args, **kwargs):
|
|
282
331
|
return (await f(*args, **kwargs)) if is_async_callable(f) else await run_in_threadpool(f, *args, **kwargs)
|
|
283
332
|
|
|
284
333
|
# %% ../nbs/api/00_core.ipynb #ad0f0e87
|
|
285
334
|
async def _wrap_ws(ws, data, params):
|
|
286
|
-
|
|
335
|
+
raw = data.pop('HEADERS', {}) or (data.pop('headers') if isinstance(data.get('headers'), dict) else {}) # htmx v4 sends lowercase `headers`
|
|
336
|
+
hdrs = Headers({k.lower():v for k,v in raw.items() if v is not None})
|
|
287
337
|
return await _find_ps(ws, data, hdrs, params)
|
|
288
338
|
|
|
289
339
|
# %% ../nbs/api/00_core.ipynb #dcc15129
|
|
@@ -321,6 +371,7 @@ def EventStream(s):
|
|
|
321
371
|
|
|
322
372
|
# %% ../nbs/api/00_core.ipynb #0dd0a414
|
|
323
373
|
def signal_shutdown():
|
|
374
|
+
"Return an `asyncio.Event` set when uvicorn receives a shutdown signal"
|
|
324
375
|
from uvicorn.main import Server
|
|
325
376
|
event = asyncio.Event()
|
|
326
377
|
@patch
|
|
@@ -504,11 +555,16 @@ htmx_exts = {
|
|
|
504
555
|
"remove-me": "https://cdn.jsdelivr.net/npm/htmx-ext-remove-me@2.0.0/remove-me.js",
|
|
505
556
|
"debug": "https://unpkg.com/htmx.org@1.9.12/dist/ext/debug.js",
|
|
506
557
|
"ws": "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.3/ws.js",
|
|
507
|
-
"
|
|
558
|
+
"ws4": "https://unpkg.com/htmx.org@4.0.0-beta6/dist/ext/hx-ws.js",
|
|
559
|
+
"chunked-transfer": "https://cdn.jsdelivr.net/npm/htmx-ext-transfer-encoding-chunked@0.4.0/transfer-encoding-chunked.js",
|
|
560
|
+
"sse4": "https://unpkg.com/htmx.org@4.0.0-beta6/dist/ext/hx-sse.js",
|
|
561
|
+
"live": "https://unpkg.com/htmx.org@4.0.0-beta6/dist/ext/hx-live.js",
|
|
562
|
+
"multipart": "https://cdn.jsdelivr.net/gh/bigskysoftware/htmx@four/src/ext/hx-multipart.min.js"
|
|
508
563
|
}
|
|
509
564
|
|
|
510
565
|
# %% ../nbs/api/00_core.ipynb #60cb52ea
|
|
511
566
|
htmxsrc = Script(src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js")
|
|
567
|
+
htmx4src = Script(src="https://unpkg.com/htmx.org@4.0.0-beta6/dist/htmx.js")
|
|
512
568
|
fhjsscr = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js")
|
|
513
569
|
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js")
|
|
514
570
|
scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js")
|
|
@@ -553,11 +609,17 @@ def qp(p:str, **kw) -> str:
|
|
|
553
609
|
return p + ('?' + urlencode({k:'' if v in (False,None) else v for k,v in kw.items()},doseq=True) if kw else '')
|
|
554
610
|
|
|
555
611
|
# %% ../nbs/api/00_core.ipynb #f86690c4
|
|
556
|
-
def def_hdrs(htmx=True, surreal=True):
|
|
612
|
+
def def_hdrs(htmx=True, htmx4=False, surreal=True):
|
|
557
613
|
"Default headers for a FastHTML app"
|
|
558
614
|
hdrs = []
|
|
559
615
|
if surreal: hdrs = [surrsrc,scopesrc] + hdrs
|
|
560
|
-
if
|
|
616
|
+
if htmx4:
|
|
617
|
+
# metaCharacter="-" makes htmx4 use dashes instead of colons
|
|
618
|
+
meta_cfg = Meta(name="htmx-config", content=json.dumps({"metaCharacter": "-"}))
|
|
619
|
+
hdrs = [meta_cfg, htmx4src,fhjsscr] + hdrs
|
|
620
|
+
# TODO: Check if fhjsscr works with htmx4
|
|
621
|
+
elif htmx:
|
|
622
|
+
hdrs = [htmxsrc,fhjsscr] + hdrs
|
|
561
623
|
return [charset, viewport] + hdrs
|
|
562
624
|
|
|
563
625
|
# %% ../nbs/api/00_core.ipynb #2c5285ae
|
|
@@ -568,11 +630,24 @@ iframe_scr = Script(NotStr("""
|
|
|
568
630
|
function sendmsg() {
|
|
569
631
|
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
|
|
570
632
|
}
|
|
633
|
+
|
|
571
634
|
window.onload = function() {
|
|
572
635
|
sendmsg();
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
636
|
+
|
|
637
|
+
const ver = window.htmx?.version || '';
|
|
638
|
+
const isV4 = ver.startsWith('4.');
|
|
639
|
+
const mc = window.htmx?.config?.metaCharacter || ':';
|
|
640
|
+
|
|
641
|
+
if (isV4) {
|
|
642
|
+
document.body.addEventListener(`htmx${mc}after${mc}swap`, sendmsg);
|
|
643
|
+
document.body.addEventListener(`htmx${mc}after${mc}ws${mc}message`, sendmsg);
|
|
644
|
+
} else {
|
|
645
|
+
document.body.addEventListener('htmx:afterSettle', sendmsg);
|
|
646
|
+
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
"""))
|
|
650
|
+
|
|
576
651
|
|
|
577
652
|
# %% ../nbs/api/00_core.ipynb #17ced9a3
|
|
578
653
|
class _LifespanCtx:
|
|
@@ -585,6 +660,7 @@ class _LifespanCtx:
|
|
|
585
660
|
async def __anext__(self): return await self.gen.__anext__()
|
|
586
661
|
|
|
587
662
|
class Lifespan:
|
|
663
|
+
"Combine `on_startup`/`on_shutdown` callbacks and an optional lifespan generator into one context manager"
|
|
588
664
|
def __init__(self, startup=None, shutdown=None, ls=None):
|
|
589
665
|
startup,shutdown = listify(startup),listify(shutdown)
|
|
590
666
|
store_attr()
|
|
@@ -603,21 +679,24 @@ class Lifespan:
|
|
|
603
679
|
|
|
604
680
|
# %% ../nbs/api/00_core.ipynb #3327a1e9
|
|
605
681
|
class FastHTML(Starlette):
|
|
682
|
+
"An HTML-first Starlette app: handler params filled from the request, FT returns rendered as pages or HTMX fragments"
|
|
606
683
|
def __init__(self, debug=False, routes=None, middleware=None, title: str = "FastHTML page", exception_handlers=None,
|
|
607
684
|
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None,
|
|
608
|
-
before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
|
|
685
|
+
before=None, after=None, surreal=True, htmx=True, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware,
|
|
609
686
|
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
610
687
|
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
|
|
611
688
|
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, max_part_size=DEF_MAXPART, **bodykw):
|
|
612
689
|
middleware,before,after = map(_list, (middleware,before,after))
|
|
613
690
|
self.title,self.canonical,self.session_cookie,self.key_fname = title,canonical,session_cookie,key_fname
|
|
614
691
|
hdrs,ftrs,exts = map(listify, (hdrs,ftrs,exts))
|
|
692
|
+
if htmx4 and exts:
|
|
693
|
+
exts = ['ws4' if e in ('ws', 'ws4') else e for e in exts]
|
|
615
694
|
exts = {k:htmx_exts[k] for k in exts}
|
|
616
695
|
htmlkw = htmlkw or {}
|
|
617
|
-
if default_hdrs: hdrs = def_hdrs(htmx, surreal=surreal) + hdrs
|
|
696
|
+
if default_hdrs: hdrs = def_hdrs(htmx, htmx4, surreal=surreal) + hdrs
|
|
618
697
|
hdrs += [Script(src=ext) for ext in exts.values()]
|
|
619
698
|
if IN_NOTEBOOK:
|
|
620
|
-
hdrs.append(iframe_scr)
|
|
699
|
+
hdrs.append(iframe_scr) # TODO: check iframe_scr work with htmx4
|
|
621
700
|
from IPython.display import display,HTML
|
|
622
701
|
if nb_hdrs: display(HTML(to_xml(tuple(hdrs))))
|
|
623
702
|
middleware.append(cors_allow)
|
|
@@ -627,8 +706,8 @@ class FastHTML(Starlette):
|
|
|
627
706
|
self.secret_key = get_key(secret_key, key_fname)
|
|
628
707
|
if sess_cls:
|
|
629
708
|
sess = Middleware(sess_cls, secret_key=self.secret_key,session_cookie=session_cookie,
|
|
630
|
-
|
|
631
|
-
|
|
709
|
+
max_age=max_age, path=sess_path, same_site=same_site,
|
|
710
|
+
https_only=sess_https_only, domain=sess_domain)
|
|
632
711
|
middleware.append(sess)
|
|
633
712
|
exception_handlers = ifnone(exception_handlers, {})
|
|
634
713
|
if 404 not in exception_handlers:
|
|
@@ -810,11 +889,10 @@ def serve(
|
|
|
810
889
|
"Run the app in an async server, with live reload set as the default."
|
|
811
890
|
from uvicorn import run
|
|
812
891
|
bk = inspect.currentframe().f_back
|
|
813
|
-
glb = bk.f_globals
|
|
814
|
-
code = bk.f_code
|
|
892
|
+
glb,back = bk.f_globals,bk.f_back
|
|
815
893
|
if not appname:
|
|
816
894
|
if glb.get('__name__')=='__main__': appname = Path(glb.get('__file__', '')).stem
|
|
817
|
-
elif
|
|
895
|
+
elif back and back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
|
|
818
896
|
if appname:
|
|
819
897
|
if not port: port=int(os.getenv("PORT", default=5001))
|
|
820
898
|
link = f'http://{"localhost" if host=="0.0.0.0" else host}:{port}'
|
|
@@ -865,6 +943,7 @@ for o in ('get', 'post', 'delete', 'put', 'patch', 'options'): setattr(Client, o
|
|
|
865
943
|
|
|
866
944
|
# %% ../nbs/api/00_core.ipynb #d5223a9a
|
|
867
945
|
class RouteFuncs:
|
|
946
|
+
"Attr-access store of named route functions (HTTP verb names excluded)"
|
|
868
947
|
def __init__(self): super().__setattr__('_funcs', {})
|
|
869
948
|
def __setattr__(self, name, value): self._funcs[name] = value
|
|
870
949
|
def __getattr__(self, name):
|
|
@@ -936,6 +1015,7 @@ def cookie(key: str, value="", max_age=None, expires=None, path="/", domain=None
|
|
|
936
1015
|
|
|
937
1016
|
# %% ../nbs/api/00_core.ipynb #8816f277
|
|
938
1017
|
def reg_re_param(m, s):
|
|
1018
|
+
"Register a Starlette URL convertor named `m` matching regex `s`"
|
|
939
1019
|
cls = get_class(f'{m}Conv', sup=StringConvertor, regex=s)
|
|
940
1020
|
register_url_convertor(m, cls())
|
|
941
1021
|
|
|
@@ -960,6 +1040,7 @@ def static_route(self:FastHTML, ext='', prefix='/', static_path='.'):
|
|
|
960
1040
|
|
|
961
1041
|
# %% ../nbs/api/00_core.ipynb #f63b7a03
|
|
962
1042
|
class StaticNoCache(StaticFiles):
|
|
1043
|
+
"StaticFiles that sends `Cache-Control: no-cache`, so browsers revalidate on every use"
|
|
963
1044
|
def file_response(self, *args, **kwargs):
|
|
964
1045
|
resp = super().file_response(*args, **kwargs)
|
|
965
1046
|
resp.headers.setdefault("Cache-Control", "no-cache")
|
|
@@ -995,7 +1076,7 @@ def add_sig_param(f, name, typ=NoneType, kind=Parameter.KEYWORD_ONLY, default=Pa
|
|
|
995
1076
|
sig = signature(f)
|
|
996
1077
|
if name in sig.parameters: return f
|
|
997
1078
|
kw = {} if default is Parameter.empty else {'default': default}
|
|
998
|
-
new_params = list(sig.parameters.values()) + [Parameter(name, kind, **kw)]
|
|
1079
|
+
new_params = list(sig.parameters.values()) + [Parameter(name, kind, annotation=typ, **kw)]
|
|
999
1080
|
f.__signature__ = sig.replace(parameters=new_params)
|
|
1000
1081
|
f.__annotations__[name] = typ
|
|
1001
1082
|
return f
|
|
@@ -1024,11 +1105,11 @@ class into:
|
|
|
1024
1105
|
|
|
1025
1106
|
# %% ../nbs/api/00_core.ipynb #1960d7ff
|
|
1026
1107
|
class MiddlewareBase:
|
|
1108
|
+
"Base for middleware: passes non-http/ws scopes through, else returns the `HTTPConnection`"
|
|
1027
1109
|
async def __call__(self, scope, receive, send) -> None:
|
|
1028
|
-
if scope["type"]
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
return HTTPConnection(scope)
|
|
1110
|
+
if scope["type"] in ("http", "websocket"): return HTTPConnection(scope)
|
|
1111
|
+
await self._app(scope, receive, send)
|
|
1112
|
+
|
|
1032
1113
|
|
|
1033
1114
|
# %% ../nbs/api/00_core.ipynb #83a20f93
|
|
1034
1115
|
class FtResponse:
|
|
@@ -1047,6 +1128,7 @@ class FtResponse:
|
|
|
1047
1128
|
|
|
1048
1129
|
# %% ../nbs/api/00_core.ipynb #9dc1025e
|
|
1049
1130
|
def unqid(seeded=False):
|
|
1131
|
+
"Random unique id, base64-encoded and prefixed with '_' so it's usable as an HTML id"
|
|
1050
1132
|
id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4()
|
|
1051
1133
|
res = b64encode(id4.bytes)
|
|
1052
1134
|
return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-'))
|
|
@@ -44,6 +44,7 @@ def fast_app(
|
|
|
44
44
|
pico:Optional[bool]=None, # Include PicoCSS header?
|
|
45
45
|
surreal:Optional[bool]=True, # Include surreal.js/scope headers?
|
|
46
46
|
htmx:Optional[bool]=True, # Include HTMX header?
|
|
47
|
+
htmx4:Optional[bool]=False,
|
|
47
48
|
exts:Optional[list|str]=None, # HTMX extension names to include
|
|
48
49
|
canonical:bool=True, # Automatically include canonical link?
|
|
49
50
|
secret_key:Optional[str]=None, # Signing key for sessions
|
|
@@ -70,7 +71,7 @@ def fast_app(
|
|
|
70
71
|
app = _app_factory(hdrs=h, ftrs=ftrs, before=before, middleware=middleware, live=live, debug=debug, title=title, routes=routes, exception_handlers=exception_handlers,
|
|
71
72
|
on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, default_hdrs=default_hdrs, secret_key=secret_key, canonical=canonical,
|
|
72
73
|
session_cookie=session_cookie, max_age=max_age, sess_path=sess_path, same_site=same_site, sess_https_only=sess_https_only,
|
|
73
|
-
sess_domain=sess_domain, key_fname=key_fname, exts=exts, surreal=surreal, htmx=htmx, htmlkw=htmlkw,
|
|
74
|
+
sess_domain=sess_domain, key_fname=key_fname, exts=exts, surreal=surreal, htmx=htmx, htmx4=htmx4, htmlkw=htmlkw,
|
|
74
75
|
reload_attempts=reload_attempts, reload_interval=reload_interval, body_wrap=body_wrap, nb_hdrs=nb_hdrs, **(bodykw or {}))
|
|
75
76
|
app.static_route_exts(static_path=static_path)
|
|
76
77
|
if not db_file: return app,app.route
|
|
@@ -86,4 +87,3 @@ def fast_app(
|
|
|
86
87
|
dbtbls = [_get_tbl(db.t, k, v) for k,v in tbls.items()]
|
|
87
88
|
if len(dbtbls)==1: dbtbls=dbtbls[0]
|
|
88
89
|
return app,app.route,*dbtbls
|
|
89
|
-
|
|
@@ -55,13 +55,17 @@ def render_ft(**kw):
|
|
|
55
55
|
def htmx_config_port(port=8000):
|
|
56
56
|
display(HTML('''
|
|
57
57
|
<script>
|
|
58
|
-
document.body.addEventListener(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
58
|
+
["htmx:configRequest","htmx-config-request"].forEach(evt => document.body.addEventListener(evt, (event) => {
|
|
59
|
+
const ctx = event.detail.ctx;
|
|
60
|
+
const path = ctx ? ctx.request.action : event.detail.path;
|
|
61
|
+
if (path.includes('://')) return;
|
|
62
|
+
const full = `${location.protocol}//${location.hostname}:%s${path}`;
|
|
63
|
+
if (ctx) { ctx.request.mode = "cors"; ctx.request.action = full; }
|
|
64
|
+
else { htmx.config.selfRequestsOnly = false; event.detail.path = full; }
|
|
65
|
+
}));
|
|
63
66
|
</script>''' % port))
|
|
64
67
|
|
|
68
|
+
|
|
65
69
|
# %% ../nbs/api/06_jupyter.ipynb #79406618
|
|
66
70
|
class JupyUvi:
|
|
67
71
|
"Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
|
|
@@ -73,7 +77,7 @@ class JupyUvi:
|
|
|
73
77
|
if live: self._setup_live(app)
|
|
74
78
|
if start: self.start()
|
|
75
79
|
if not os.environ.get('IN_SOLVEIT'): htmx_config_port(port)
|
|
76
|
-
|
|
80
|
+
|
|
77
81
|
def start(self):
|
|
78
82
|
self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs)
|
|
79
83
|
|
|
@@ -21,10 +21,10 @@ try: from IPython import display
|
|
|
21
21
|
except ImportError: display=None
|
|
22
22
|
|
|
23
23
|
# %% ../nbs/api/04_pico.ipynb #100414ae
|
|
24
|
-
picocss = "https://cdn.jsdelivr.net/npm/@
|
|
24
|
+
picocss = "https://cdn.jsdelivr.net/npm/@anyblades/pico@latest/css/pico.min.css"
|
|
25
25
|
picolink = (Link(rel="stylesheet", href=picocss),
|
|
26
26
|
Style(":root { --pico-font-size: 100%; }"))
|
|
27
|
-
picocondcss = "https://cdn.jsdelivr.net/npm/@
|
|
27
|
+
picocondcss = "https://cdn.jsdelivr.net/npm/@anyblades/pico@latest/css/pico.conditional.min.css"
|
|
28
28
|
picocondlink = (Link(rel="stylesheet", href=picocondcss),
|
|
29
29
|
Style(":root { --pico-font-size: 100%; }"))
|
|
30
30
|
|
|
@@ -37,8 +37,7 @@ new MutationObserver(ms => {
|
|
|
37
37
|
ms.forEach(m => {
|
|
38
38
|
m.addedNodes.forEach(n => {
|
|
39
39
|
if (n.nodeType === 1) {
|
|
40
|
-
|
|
41
|
-
if (nc && (nc.contains('cell-output') || nc.contains('output_area'))) nc.add('pico');
|
|
40
|
+
if (n.matches(sel)) n.classList.add('pico');
|
|
42
41
|
n.querySelectorAll(sel).forEach(e => e.classList.add('pico'));
|
|
43
42
|
}
|
|
44
43
|
});
|
|
@@ -86,7 +85,8 @@ def Container(*args, **kwargs)->FT:
|
|
|
86
85
|
"A PicoCSS Container, implemented as a Main with class 'container'"
|
|
87
86
|
return Main(*args, cls="container", **kwargs)
|
|
88
87
|
|
|
89
|
-
# %% ../nbs/api/04_pico.ipynb #
|
|
88
|
+
# %% ../nbs/api/04_pico.ipynb #b8c98614
|
|
90
89
|
def PicoBusy():
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
elt = "(event.detail.ctx?.sourceElement || event.detail.elt)"
|
|
91
|
+
return (HtmxOn('beforeRequest', f"{elt}.setAttribute('aria-busy', 'true' )"),
|
|
92
|
+
HtmxOn('afterRequest', f"{elt}.setAttribute('aria-busy', 'false')"))
|
|
@@ -167,9 +167,10 @@ def run_js(js, id=None, **kw):
|
|
|
167
167
|
|
|
168
168
|
# %% ../nbs/api/02_xtend.ipynb #365f57a8
|
|
169
169
|
def HtmxOn(eventname:str, code:str):
|
|
170
|
+
"Run `code` on htmx event `eventname`; both the v2 (`htmx:camelCase`) and v4 (`htmx-dash-case`) spellings are bound, and only the loaded version's fires"
|
|
170
171
|
return Script('''domReadyExecute(function() {
|
|
171
|
-
document.body.addEventListener(
|
|
172
|
-
})''' % (eventname, code))
|
|
172
|
+
["htmx:%s","htmx-%s"].forEach(evt => document.body.addEventListener(evt, function(event) { %s }))
|
|
173
|
+
})''' % (to_camel(eventname), to_kebab(eventname), code))
|
|
173
174
|
|
|
174
175
|
# %% ../nbs/api/02_xtend.ipynb #39f20784
|
|
175
176
|
def jsd(org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, **kwargs)->FT:
|
|
@@ -242,23 +243,24 @@ def Favicon(light_icon, dark_icon):
|
|
|
242
243
|
# %% ../nbs/api/02_xtend.ipynb #50444181
|
|
243
244
|
def clear(id): return Div(hx_swap_oob='innerHTML', id=id)
|
|
244
245
|
|
|
245
|
-
# %% ../nbs/api/02_xtend.ipynb #
|
|
246
|
-
sid_scr = Script(
|
|
246
|
+
# %% ../nbs/api/02_xtend.ipynb #f46160fb
|
|
247
|
+
sid_scr = Script("""
|
|
247
248
|
function uuid() {
|
|
248
249
|
return [...crypto.getRandomValues(new Uint8Array(10))].map(b=>b.toString(36)).join('');
|
|
249
250
|
}
|
|
250
|
-
|
|
251
251
|
sessionStorage.setItem("sid", sessionStorage.getItem("sid") || uuid());
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
253
|
+
function addSid(url, sid) {
|
|
254
|
+
const u = new URL(url, window.location.origin);
|
|
255
|
+
u.searchParams.set('sid', sid);
|
|
256
|
+
return u.pathname + u.search;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const mc = htmx.config?.metaCharacter || ':';
|
|
260
|
+
|
|
261
|
+
htmx.on("htmx:configRequest", (e) => { const sid = sessionStorage.getItem("sid"); if (sid) e.detail.path = addSid(e.detail.path, sid); }); // htmx v2
|
|
262
|
+
htmx.on(`htmx${mc}config${mc}request`, (e) => { const sid = sessionStorage.getItem("sid"); if (sid) e.detail.ctx.request.action = addSid(e.detail.ctx.request.action, sid); }); // htmx v4
|
|
263
|
+
""")
|
|
262
264
|
|
|
263
265
|
# %% ../nbs/api/02_xtend.ipynb #579e1f33
|
|
264
266
|
def with_sid(app, dest, path='/'):
|
|
@@ -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>=2.
|
|
15
|
+
dependencies = ['fastcore>=2.2.7', 'python-dateutil', 'starlette>=1.0.1', 'oauthlib', 'itsdangerous', 'uvicorn[standard]>=0.30', 'httpx2', 'python-multipart', 'beautifulsoup4']
|
|
16
16
|
|
|
17
17
|
[project.urls]
|
|
18
18
|
Repository = "https://github.com/AnswerDotAI/fasthtml"
|
|
@@ -45,4 +45,3 @@ lib_path = "fasthtml"
|
|
|
45
45
|
[tool.chkstyle]
|
|
46
46
|
skip_paths = ["_modidx.py"]
|
|
47
47
|
ignore = ["closing-bracket", "inefficient-multiline-signature", "inefficient-multiline-expression"]
|
|
48
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.12
|
|
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>=2.
|
|
18
|
+
Requires-Dist: fastcore>=2.2.7
|
|
19
19
|
Requires-Dist: python-dateutil
|
|
20
20
|
Requires-Dist: starlette>=1.0.1
|
|
21
21
|
Requires-Dist: oauthlib
|
|
@@ -128,7 +128,6 @@ The capabilities of FastHTML are vast and growing, and not all the features and
|
|
|
128
128
|
Then explore the small but growing third-party ecosystem of FastHTML tutorials, notebooks, libraries, and components:
|
|
129
129
|
|
|
130
130
|
- [FastHTML Gallery](https://gallery.fastht.ml): Learn from minimal examples of components (ie chat bubbles, click-to-edit, infinite scroll, etc)
|
|
131
|
-
- [Creating Custom FastHTML Tags for Markdown Rendering](https://isaac-flath.github.io/website/posts/boots/FasthtmlTutorial.html) by Isaac Flath
|
|
132
131
|
- [How to Build a Simple Login System in FastHTML](https://blog.mariusvach.com/posts/login-fasthtml) by Marius Vach
|
|
133
132
|
- Your tutorial here!
|
|
134
133
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{python_fasthtml-0.14.11 → python_fasthtml-0.14.12}/python_fasthtml.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|