python-fasthtml 0.14.12__tar.gz → 0.14.14__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.12 → python_fasthtml-0.14.14}/CONTRIBUTING.md +5 -5
- {python_fasthtml-0.14.12/python_fasthtml.egg-info → python_fasthtml-0.14.14}/PKG-INFO +2 -2
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/__init__.py +1 -1
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/_modidx.py +2 -9
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/core.py +58 -48
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/oauth.py +4 -1
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/pyproject.toml +2 -2
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14/python_fasthtml.egg-info}/PKG-INFO +2 -2
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/LICENSE +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/MANIFEST.in +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/README.md +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/basics.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/cli.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/common.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/components.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/components.pyi +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/core.pyi +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/fastapp.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/ft.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/js.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/jupyter.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/katex.js +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/pico.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/stripe_otp.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/svg.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/xtend.py +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/fasthtml/xtend.pyi +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/SOURCES.txt +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/requires.txt +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/setup.cfg +0 -0
- {python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/tests/test_toaster.py +0 -0
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
Make sure you have read the [doc on code style](
|
|
4
4
|
https://docs.fast.ai/dev/style.html) first. (Note that we don't follow PEP8, but instead follow a coding style designed specifically for numerical and interactive programming.)
|
|
5
5
|
|
|
6
|
-
This project uses [nbdev](https://nbdev.fast.ai/getting_started.html) for development. Before beginning, make sure that nbdev and a jupyter-compatible client such as jupyterlab or nbclassic are installed. To make changes to the codebase, update the notebooks in the `nbs` folder, not the .py files directly. Then, run `
|
|
6
|
+
This project uses [nbdev](https://nbdev.fast.ai/getting_started.html) for development. Before beginning, make sure that nbdev and a jupyter-compatible client such as jupyterlab or nbclassic are installed. To make changes to the codebase, update the notebooks in the `nbs` folder, not the .py files directly. Then, run `nbdev-export`. For more details, have a look at the [nbdev tutorial](https://nbdev.fast.ai/tutorials/tutorial.html). Depending on the code changes, you might also need to run `tools/update.sh` to update python interface modules and [LLM reference material](https://github.com/AnswerDotAI/llms-txt).
|
|
7
7
|
|
|
8
8
|
You may want to set up a `prep` alias in `~/.zshrc` or other shell startup file:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
alias prep='
|
|
11
|
+
alias prep='nbdev-export && nbdev-clean && nbdev-trust'
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Run `prep` before each commit to ensure your python files are up to date, and
|
|
14
|
+
Run `prep` before each commit to ensure your python files are up to date, and your notebooks cleaned of metadata and notarized.
|
|
15
15
|
|
|
16
16
|
## Updating README.md
|
|
17
17
|
|
|
@@ -26,13 +26,13 @@ pip install -e '.[dev]'
|
|
|
26
26
|
And install quarto too:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
29
|
+
nbdev-install-quarto
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Then, after you make subsequent changes to `nbs/index.ipynb`, run the following from the repo's root directory to (re)build `README.md`:
|
|
33
33
|
|
|
34
34
|
```
|
|
35
|
-
|
|
35
|
+
nbdev-readme
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Did you find a bug?
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.14
|
|
4
4
|
Summary: The fastest way to create an HTML app
|
|
5
5
|
Author-email: Jeremy Howard and contributors <github@jhoward.fastmail.fm>
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Repository, https://github.com/AnswerDotAI/fasthtml
|
|
8
8
|
Project-URL: Documentation, https://www.fastht.ml/docs/
|
|
9
9
|
Keywords: nbdev,jupyter,notebook,python
|
|
@@ -5,12 +5,9 @@ d = { 'settings': { 'branch': 'main',
|
|
|
5
5
|
'doc_host': 'https://www.fastht.ml',
|
|
6
6
|
'git_url': 'https://github.com/AnswerDotAI/fasthtml',
|
|
7
7
|
'lib_path': 'fasthtml'},
|
|
8
|
-
'syms': { 'fasthtml.
|
|
9
|
-
'fasthtml.basics': {},
|
|
10
|
-
'fasthtml.cli': { 'fasthtml.cli._run': ('api/cli.html#_run', 'fasthtml/cli.py'),
|
|
8
|
+
'syms': { 'fasthtml.cli': { 'fasthtml.cli._run': ('api/cli.html#_run', 'fasthtml/cli.py'),
|
|
11
9
|
'fasthtml.cli.railway_deploy': ('api/cli.html#railway_deploy', 'fasthtml/cli.py'),
|
|
12
10
|
'fasthtml.cli.railway_link': ('api/cli.html#railway_link', 'fasthtml/cli.py')},
|
|
13
|
-
'fasthtml.common': {},
|
|
14
11
|
'fasthtml.components': { 'fasthtml.components.FT.__add__': ('api/components.html#ft.__add__', 'fasthtml/components.py'),
|
|
15
12
|
'fasthtml.components.FT.__radd__': ('api/components.html#ft.__radd__', 'fasthtml/components.py'),
|
|
16
13
|
'fasthtml.components.FT.__str__': ('api/components.html#ft.__str__', 'fasthtml/components.py'),
|
|
@@ -130,6 +127,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
130
127
|
'fasthtml.core._resp': ('api/core.html#_resp', 'fasthtml/core.py'),
|
|
131
128
|
'fasthtml.core._route_pn': ('api/core.html#_route_pn', 'fasthtml/core.py'),
|
|
132
129
|
'fasthtml.core._send_ws': ('api/core.html#_send_ws', 'fasthtml/core.py'),
|
|
130
|
+
'fasthtml.core._spawn': ('api/core.html#_spawn', 'fasthtml/core.py'),
|
|
133
131
|
'fasthtml.core._to_htmx_header': ('api/core.html#_to_htmx_header', 'fasthtml/core.py'),
|
|
134
132
|
'fasthtml.core._to_xml': ('api/core.html#_to_xml', 'fasthtml/core.py'),
|
|
135
133
|
'fasthtml.core._url_for': ('api/core.html#_url_for', 'fasthtml/core.py'),
|
|
@@ -168,8 +166,6 @@ d = { 'settings': { 'branch': 'main',
|
|
|
168
166
|
'fasthtml.core.until_disconnect': ('api/core.html#until_disconnect', 'fasthtml/core.py'),
|
|
169
167
|
'fasthtml.core.uri': ('api/core.html#uri', 'fasthtml/core.py'),
|
|
170
168
|
'fasthtml.core.vurl': ('api/core.html#vurl', 'fasthtml/core.py')},
|
|
171
|
-
'fasthtml.fastapp': {},
|
|
172
|
-
'fasthtml.ft': {},
|
|
173
169
|
'fasthtml.js': { 'fasthtml.js.HighlightJS': ('api/js.html#highlightjs', 'fasthtml/js.py'),
|
|
174
170
|
'fasthtml.js.KatexMarkdownJS': ('api/js.html#katexmarkdownjs', 'fasthtml/js.py'),
|
|
175
171
|
'fasthtml.js.MarkdownJS': ('api/js.html#markdownjs', 'fasthtml/js.py'),
|
|
@@ -195,7 +191,6 @@ d = { 'settings': { 'branch': 'main',
|
|
|
195
191
|
'fasthtml.jupyter.render_ft': ('api/jupyter.html#render_ft', 'fasthtml/jupyter.py'),
|
|
196
192
|
'fasthtml.jupyter.show': ('api/jupyter.html#show', 'fasthtml/jupyter.py'),
|
|
197
193
|
'fasthtml.jupyter.ws_client': ('api/jupyter.html#ws_client', 'fasthtml/jupyter.py')},
|
|
198
|
-
'fasthtml.live_reload': {},
|
|
199
194
|
'fasthtml.oauth': { 'fasthtml.oauth.AppleAppClient': ('api/oauth.html#appleappclient', 'fasthtml/oauth.py'),
|
|
200
195
|
'fasthtml.oauth.AppleAppClient.__init__': ('api/oauth.html#appleappclient.__init__', 'fasthtml/oauth.py'),
|
|
201
196
|
'fasthtml.oauth.AppleAppClient.client_secret': ( 'api/oauth.html#appleappclient.client_secret',
|
|
@@ -264,7 +259,6 @@ d = { 'settings': { 'branch': 'main',
|
|
|
264
259
|
'fasthtml.pico.PicoBusy': ('api/pico.html#picobusy', 'fasthtml/pico.py'),
|
|
265
260
|
'fasthtml.pico.Search': ('api/pico.html#search', 'fasthtml/pico.py'),
|
|
266
261
|
'fasthtml.pico.set_pico_cls': ('api/pico.html#set_pico_cls', 'fasthtml/pico.py')},
|
|
267
|
-
'fasthtml.starlette': {},
|
|
268
262
|
'fasthtml.stripe_otp': { 'fasthtml.stripe_otp.Payment': ('explains/stripe.html#payment', 'fasthtml/stripe_otp.py'),
|
|
269
263
|
'fasthtml.stripe_otp._search_app': ('explains/stripe.html#_search_app', 'fasthtml/stripe_otp.py'),
|
|
270
264
|
'fasthtml.stripe_otp.account_management': ( 'explains/stripe.html#account_management',
|
|
@@ -304,7 +298,6 @@ d = { 'settings': { 'branch': 'main',
|
|
|
304
298
|
'fasthtml.svg.Text': ('api/svg.html#text', 'fasthtml/svg.py'),
|
|
305
299
|
'fasthtml.svg.ft_svg': ('api/svg.html#ft_svg', 'fasthtml/svg.py'),
|
|
306
300
|
'fasthtml.svg.transformd': ('api/svg.html#transformd', 'fasthtml/svg.py')},
|
|
307
|
-
'fasthtml.toaster': {},
|
|
308
301
|
'fasthtml.xtend': { 'fasthtml.xtend.A': ('api/xtend.html#a', 'fasthtml/xtend.py'),
|
|
309
302
|
'fasthtml.xtend.AX': ('api/xtend.html#ax', 'fasthtml/xtend.py'),
|
|
310
303
|
'fasthtml.xtend.AnyNow': ('api/xtend.html#anynow', 'fasthtml/xtend.py'),
|
|
@@ -18,7 +18,7 @@ The decorated function doubles as a URL builder, so links never need hardcoded p
|
|
|
18
18
|
|
|
19
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
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:
|
|
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. Hyphens in form/query names match underscores in handler parameters and typed body fields. Exact names take precedence within each source. Dictionary and request access retain the original keys. `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
22
|
|
|
23
23
|
req, ws the Request / WebSocket connection
|
|
24
24
|
sess session dict
|
|
@@ -55,7 +55,7 @@ __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'DEF_MAXPART', 'htmx_ex
|
|
|
55
55
|
'MiddlewareBase', 'FtResponse', 'unqid']
|
|
56
56
|
|
|
57
57
|
# %% ../nbs/api/00_core.ipynb #23503b9e
|
|
58
|
-
import json,uuid,inspect,types,asyncio,inspect,random,contextlib,itsdangerous,hashlib
|
|
58
|
+
import json,uuid,inspect,types,asyncio,inspect,random,contextlib,itsdangerous,hashlib,signal,subprocess
|
|
59
59
|
from uuid import uuid5, NAMESPACE_URL
|
|
60
60
|
|
|
61
61
|
from fastcore.utils import *
|
|
@@ -80,6 +80,7 @@ from base64 import b64encode,b64decode
|
|
|
80
80
|
from email.utils import format_datetime
|
|
81
81
|
|
|
82
82
|
from .starlette import *
|
|
83
|
+
from python_multipart.exceptions import MultipartParseError
|
|
83
84
|
|
|
84
85
|
# %% ../nbs/api/00_core.ipynb #19d3f2a7
|
|
85
86
|
def _params(f): return signature_ex(f, True).parameters
|
|
@@ -169,7 +170,7 @@ class HttpHeader:
|
|
|
169
170
|
def _to_htmx_header(s): return 'HX-' + s.replace('_', '-').title()
|
|
170
171
|
|
|
171
172
|
htmx_resps = dict(location=None, push_url=None, redirect=None, refresh=None, replace_url=None,
|
|
172
|
-
|
|
173
|
+
reswap=None, retarget=None, reselect=None, trigger=None, trigger_after_settle=None, trigger_after_swap=None)
|
|
173
174
|
|
|
174
175
|
# %% ../nbs/api/00_core.ipynb #f6a2e62e
|
|
175
176
|
@use_kwargs_dict(**htmx_resps)
|
|
@@ -189,7 +190,8 @@ def _is_body(anno): return issubclass(anno, (dict,ns)) or hasattr(anno,'__from_r
|
|
|
189
190
|
|
|
190
191
|
# %% ../nbs/api/00_core.ipynb #ffdde66f
|
|
191
192
|
def _formitem(form, k):
|
|
192
|
-
"Return
|
|
193
|
+
"Return item `k`, accepting hyphens for underscores and unwrapping single-item form lists"
|
|
194
|
+
if k not in form: k = {o.replace('-', '_'):o for o in form}.get(k, k)
|
|
193
195
|
if isinstance(form, dict): return form.get(k)
|
|
194
196
|
o = form.getlist(k)
|
|
195
197
|
return o[0] if len(o) == 1 else o if o else None
|
|
@@ -205,16 +207,21 @@ DEF_MAXPART = 100*1024*1024
|
|
|
205
207
|
|
|
206
208
|
# %% ../nbs/api/00_core.ipynb #42c9cea0
|
|
207
209
|
async def parse_form(req: Request) -> FormData:
|
|
208
|
-
"Starlette errors on empty multipart/json forms, so this
|
|
210
|
+
"Starlette errors on empty multipart/json forms and 500s on malformed multipart, so this handles those situations"
|
|
209
211
|
ctype = req.headers.get("Content-Type", "")
|
|
210
212
|
maxpart = getattr(req, "max_part_size", DEF_MAXPART)
|
|
211
213
|
if ctype.startswith("multipart/form-data"):
|
|
212
214
|
try: boundary = ctype.split("boundary=")[1].strip()
|
|
213
215
|
except IndexError: raise HTTPException(400, "Invalid form-data: no boundary")
|
|
214
216
|
if int(req.headers.get("Content-Length", "0")) <= len(boundary) + 6: return FormData()
|
|
215
|
-
return await req.form(max_part_size=maxpart)
|
|
217
|
+
try: return await req.form(max_part_size=maxpart)
|
|
218
|
+
except MultipartParseError as e: raise HTTPException(400, f"Invalid form-data: {e}")
|
|
216
219
|
body = await req.body() # Cache body for non-multipart request types
|
|
217
|
-
if ctype == 'application/json':
|
|
220
|
+
if ctype == 'application/json':
|
|
221
|
+
try: res = await req.json() if body else {}
|
|
222
|
+
except ValueError: raise HTTPException(400, "Invalid JSON body")
|
|
223
|
+
if not isinstance(res, dict): raise HTTPException(400, "JSON body must be an object")
|
|
224
|
+
return res
|
|
218
225
|
return await req.form(max_part_size=maxpart)
|
|
219
226
|
|
|
220
227
|
# %% ../nbs/api/00_core.ipynb #0caedd04
|
|
@@ -229,6 +236,7 @@ async def _from_body(conn, p, data):
|
|
|
229
236
|
kwargs = await _find_ps(conn, data, conn.headers, ps)
|
|
230
237
|
return await maybe_await(ctor(**kwargs))
|
|
231
238
|
d = _annotations(anno)
|
|
239
|
+
if d: data = {k.replace('-', '_'):v for k,v in data.items()} | data
|
|
232
240
|
cargs = {k: _form_arg(k, v, d) for k, v in data.items() if not d or k in d}
|
|
233
241
|
return anno(**cargs)
|
|
234
242
|
|
|
@@ -286,10 +294,9 @@ async def _find_p(conn, data, hdrs, arg:str, p:Parameter):
|
|
|
286
294
|
res = conn.path_params.get(arg, None)
|
|
287
295
|
if res in (empty,None): res = conn.cookies.get(arg, None)
|
|
288
296
|
if res in (empty,None): res = hdrs.get(snake2hyphens(arg), None)
|
|
289
|
-
if res in (empty,None): res = conn.query_params
|
|
290
|
-
if res
|
|
291
|
-
if res in (empty,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`
|
|
297
|
+
if res in (empty,None): res = _formitem(conn.query_params, arg)
|
|
298
|
+
if res in (empty,None): res = _formitem(data, arg)
|
|
299
|
+
if res in (empty,None) and isinstance(vals := data.get('values'), dict): res = _formitem(vals, arg) # htmx v4 nests form data under `values`
|
|
293
300
|
if res in (empty,None):
|
|
294
301
|
if p.default is empty:
|
|
295
302
|
if isinstance(conn, Request): raise HTTPException(400, f"Missing required field: {arg}")
|
|
@@ -544,6 +551,7 @@ async def _wrap_call(f, req, params):
|
|
|
544
551
|
return await _handle(f, **wreq)
|
|
545
552
|
|
|
546
553
|
# %% ../nbs/api/00_core.ipynb #b0d1cbbf
|
|
554
|
+
# chkstyle: skip
|
|
547
555
|
htmx_exts = {
|
|
548
556
|
"morph": "https://cdn.jsdelivr.net/npm/idiomorph@0.7.3/dist/idiomorph-ext.min.js",
|
|
549
557
|
"head-support": "https://cdn.jsdelivr.net/npm/htmx-ext-head-support@2.0.4/head-support.js",
|
|
@@ -555,16 +563,16 @@ htmx_exts = {
|
|
|
555
563
|
"remove-me": "https://cdn.jsdelivr.net/npm/htmx-ext-remove-me@2.0.0/remove-me.js",
|
|
556
564
|
"debug": "https://unpkg.com/htmx.org@1.9.12/dist/ext/debug.js",
|
|
557
565
|
"ws": "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.3/ws.js",
|
|
558
|
-
"ws4": "https://
|
|
566
|
+
"ws4": "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-ws.min.js",
|
|
559
567
|
"chunked-transfer": "https://cdn.jsdelivr.net/npm/htmx-ext-transfer-encoding-chunked@0.4.0/transfer-encoding-chunked.js",
|
|
560
|
-
"sse4": "https://
|
|
561
|
-
"live": "https://
|
|
562
|
-
"multipart": "https://cdn.jsdelivr.net/
|
|
568
|
+
"sse4": "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-sse.min.js",
|
|
569
|
+
"live": "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-live.min.js",
|
|
570
|
+
"multipart": "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-multipart.min.js"
|
|
563
571
|
}
|
|
564
572
|
|
|
565
573
|
# %% ../nbs/api/00_core.ipynb #60cb52ea
|
|
566
574
|
htmxsrc = Script(src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js")
|
|
567
|
-
htmx4src = Script(src="https://
|
|
575
|
+
htmx4src = Script(src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js")
|
|
568
576
|
fhjsscr = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js")
|
|
569
577
|
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js")
|
|
570
578
|
scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js")
|
|
@@ -618,13 +626,11 @@ def def_hdrs(htmx=True, htmx4=False, surreal=True):
|
|
|
618
626
|
meta_cfg = Meta(name="htmx-config", content=json.dumps({"metaCharacter": "-"}))
|
|
619
627
|
hdrs = [meta_cfg, htmx4src,fhjsscr] + hdrs
|
|
620
628
|
# TODO: Check if fhjsscr works with htmx4
|
|
621
|
-
elif htmx:
|
|
622
|
-
hdrs = [htmxsrc,fhjsscr] + hdrs
|
|
629
|
+
elif htmx: hdrs = [htmxsrc,fhjsscr] + hdrs
|
|
623
630
|
return [charset, viewport] + hdrs
|
|
624
631
|
|
|
625
632
|
# %% ../nbs/api/00_core.ipynb #2c5285ae
|
|
626
|
-
cors_allow = Middleware(CORSMiddleware, allow_credentials=True,
|
|
627
|
-
allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
633
|
+
cors_allow = Middleware(CORSMiddleware, allow_credentials=True, allow_origins=['*'], allow_methods=['*'], allow_headers=['*'])
|
|
628
634
|
|
|
629
635
|
iframe_scr = Script(NotStr("""
|
|
630
636
|
function sendmsg() {
|
|
@@ -674,23 +680,21 @@ class Lifespan:
|
|
|
674
680
|
else: yield
|
|
675
681
|
for f in self.shutdown: await _handle(f)
|
|
676
682
|
|
|
677
|
-
def on_event(self, event_type):
|
|
678
|
-
return lambda f: (getattr(self, event_type)).append(f)
|
|
683
|
+
def on_event(self, event_type): return lambda f: (getattr(self, event_type)).append(f)
|
|
679
684
|
|
|
680
685
|
# %% ../nbs/api/00_core.ipynb #3327a1e9
|
|
681
686
|
class FastHTML(Starlette):
|
|
682
687
|
"An HTML-first Starlette app: handler params filled from the request, FT returns rendered as pages or HTMX fragments"
|
|
683
688
|
def __init__(self, debug=False, routes=None, middleware=None, title: str = "FastHTML page", exception_handlers=None,
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
+
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None,
|
|
690
|
+
before=None, after=None, surreal=True, htmx=True, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware,
|
|
691
|
+
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
692
|
+
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
|
|
693
|
+
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, max_part_size=DEF_MAXPART, **bodykw):
|
|
689
694
|
middleware,before,after = map(_list, (middleware,before,after))
|
|
690
695
|
self.title,self.canonical,self.session_cookie,self.key_fname = title,canonical,session_cookie,key_fname
|
|
691
696
|
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]
|
|
697
|
+
if htmx4 and exts: exts = ['ws4' if e in ('ws', 'ws4') else e for e in exts]
|
|
694
698
|
exts = {k:htmx_exts[k] for k in exts}
|
|
695
699
|
htmlkw = htmlkw or {}
|
|
696
700
|
if default_hdrs: hdrs = def_hdrs(htmx, htmx4, surreal=surreal) + hdrs
|
|
@@ -705,9 +709,8 @@ class FastHTML(Starlette):
|
|
|
705
709
|
self.body_wrap,self.before,self.after,self.htmlkw,self.bodykw,self.max_part_size = body_wrap,before,after,htmlkw,bodykw,max_part_size
|
|
706
710
|
self.secret_key = get_key(secret_key, key_fname)
|
|
707
711
|
if sess_cls:
|
|
708
|
-
sess = Middleware(sess_cls, secret_key=self.secret_key,session_cookie=session_cookie,
|
|
709
|
-
|
|
710
|
-
https_only=sess_https_only, domain=sess_domain)
|
|
712
|
+
sess = Middleware(sess_cls, secret_key=self.secret_key, session_cookie=session_cookie, max_age=max_age, path=sess_path,
|
|
713
|
+
same_site=same_site, https_only=sess_https_only, domain=sess_domain)
|
|
711
714
|
middleware.append(sess)
|
|
712
715
|
exception_handlers = ifnone(exception_handlers, {})
|
|
713
716
|
if 404 not in exception_handlers:
|
|
@@ -742,8 +745,8 @@ def add_route(self:FastHTML, route):
|
|
|
742
745
|
"Add or replace a route in the FastHTML app"
|
|
743
746
|
route.methods = [m.upper() for m in listify(route.methods)]
|
|
744
747
|
self.router.routes = [r for r in self.router.routes if not
|
|
745
|
-
|
|
746
|
-
|
|
748
|
+
(r.path==route.path and r.name == route.name and
|
|
749
|
+
((route.methods is None) or (set(r.methods) == set(route.methods))))]
|
|
747
750
|
self.router.routes.append(route)
|
|
748
751
|
self.router.routes.sort(key=lambda r: not getattr(r, 'host', None))
|
|
749
752
|
|
|
@@ -767,8 +770,7 @@ def _endp(self:FastHTML, f, body_wrap, before:Optional[Callable|tuple]=None):
|
|
|
767
770
|
if not resp:
|
|
768
771
|
if isinstance(b, Beforeware): bf,skip = b.f,b.skip
|
|
769
772
|
else: bf,skip = b,[]
|
|
770
|
-
if not any(re.fullmatch(r, req.url.path) for r in skip):
|
|
771
|
-
resp = await _wrap_call(bf, req, _params(bf))
|
|
773
|
+
if not any(re.fullmatch(r, req.url.path) for r in skip): resp = await _wrap_call(bf, req, _params(bf))
|
|
772
774
|
for b in listify(before):
|
|
773
775
|
if not resp: resp = await _wrap_call(b, req, _params(b))
|
|
774
776
|
req.body_wrap = body_wrap
|
|
@@ -879,12 +881,13 @@ def set_lifespan(self:FastHTML, value):
|
|
|
879
881
|
|
|
880
882
|
# %% ../nbs/api/00_core.ipynb #3a348474
|
|
881
883
|
def serve(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
884
|
+
appname=None, # Name of the module
|
|
885
|
+
app='app', # App instance to be served
|
|
886
|
+
host='0.0.0.0', # If host is 0.0.0.0 will convert to localhost
|
|
887
|
+
port=None, # If port is None it will default to 5001 or the PORT environment variable
|
|
888
|
+
reload=True, # Default is to reload the app upon code changes
|
|
889
|
+
procs=None, # Processes to run on consecutive ports from `port`; default 1 or the PROCS environment variable
|
|
890
|
+
**kwargs
|
|
888
891
|
):
|
|
889
892
|
"Run the app in an async server, with live reload set as the default."
|
|
890
893
|
from uvicorn import run
|
|
@@ -895,10 +898,20 @@ def serve(
|
|
|
895
898
|
elif back and back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
|
|
896
899
|
if appname:
|
|
897
900
|
if not port: port=int(os.getenv("PORT", default=5001))
|
|
901
|
+
if not procs: procs=int(os.getenv("PROCS", default=1))
|
|
902
|
+
if procs>1 and 'FH_PROCNUM' not in os.environ: return _spawn(procs)
|
|
903
|
+
port += int(os.getenv('FH_PROCNUM', 0))
|
|
898
904
|
link = f'http://{"localhost" if host=="0.0.0.0" else host}:{port}'
|
|
899
905
|
print('Link: '+ S.light_red.bold(link))
|
|
900
906
|
run(f'{appname}:{app}', host=host, port=port, reload=reload, **kwargs)
|
|
901
907
|
|
|
908
|
+
# %% ../nbs/api/00_core.ipynb #91b08e9a
|
|
909
|
+
def _spawn(n):
|
|
910
|
+
"Run this script `n` times over, numbering each copy in `FH_PROCNUM`, and wait for them all"
|
|
911
|
+
ps = [subprocess.Popen([sys.executable, *sys.argv], env={**os.environ, 'FH_PROCNUM':str(i)}) for i in range(n)]
|
|
912
|
+
signal.signal(signal.SIGTERM, lambda *a: [p.terminate() for p in ps])
|
|
913
|
+
for p in ps: p.wait()
|
|
914
|
+
|
|
902
915
|
# %% ../nbs/api/00_core.ipynb #c5220a0e
|
|
903
916
|
async def _wait_disconnect(req):
|
|
904
917
|
while (await req.receive())["type"] != "http.disconnect": pass
|
|
@@ -1001,8 +1014,7 @@ def cookie(key: str, value="", max_age=None, expires=None, path="/", domain=None
|
|
|
1001
1014
|
cookie = cookies.SimpleCookie()
|
|
1002
1015
|
cookie[key] = value
|
|
1003
1016
|
if max_age is not None: cookie[key]["max-age"] = max_age
|
|
1004
|
-
if expires is not None:
|
|
1005
|
-
cookie[key]["expires"] = format_datetime(expires, usegmt=True) if isinstance(expires, datetime) else expires
|
|
1017
|
+
if expires is not None: cookie[key]["expires"] = format_datetime(expires, usegmt=True) if isinstance(expires, datetime) else expires
|
|
1006
1018
|
if path is not None: cookie[key]["path"] = path
|
|
1007
1019
|
if domain is not None: cookie[key]["domain"] = domain
|
|
1008
1020
|
if secure: cookie[key]["secure"] = True
|
|
@@ -1159,8 +1171,7 @@ def devtools_json(self:FastHTML, path=None, uuid=None):
|
|
|
1159
1171
|
if not path: path = Path().absolute()
|
|
1160
1172
|
if not uuid: uuid = str(uuid5(NAMESPACE_URL, str(path)))
|
|
1161
1173
|
@self.route(devtools_loc)
|
|
1162
|
-
def devtools():
|
|
1163
|
-
return dict(workspace=dict(root=str(path), uuid=uuid))
|
|
1174
|
+
def devtools(): return dict(workspace=dict(root=str(path), uuid=uuid))
|
|
1164
1175
|
|
|
1165
1176
|
# %% ../nbs/api/00_core.ipynb #e27908c0
|
|
1166
1177
|
@patch
|
|
@@ -1192,8 +1203,7 @@ def get_testclient(self:FastHTML, **kw):
|
|
|
1192
1203
|
"A Starlette TestClient with a `session` property"
|
|
1193
1204
|
@property
|
|
1194
1205
|
def session(self):
|
|
1195
|
-
cookie = next((c.value for c in reversed(list(self.cookies.jar))
|
|
1196
|
-
if c.name == self.app.session_cookie), None)
|
|
1206
|
+
cookie = next((c.value for c in reversed(list(self.cookies.jar)) if c.name == self.app.session_cookie), None)
|
|
1197
1207
|
return self.app.decode_session(cookie)
|
|
1198
1208
|
|
|
1199
1209
|
return FastHTMLTestClient(self, cookies=self.get_client(**kw).cookies)
|
|
@@ -249,7 +249,10 @@ class OAuth:
|
|
|
249
249
|
return RedirectResponse(self.error_path, status_code=303)
|
|
250
250
|
scheme = 'http' if url_match(req,self.http_patterns) or not self.https else 'https'
|
|
251
251
|
base_url = f"{scheme}://{get_host(req)}"
|
|
252
|
-
info = AttrDictDefault(await cli.retr_info_async(code, base_url+redir_path))
|
|
252
|
+
try: info = AttrDictDefault(await cli.retr_info_async(code, base_url+redir_path))
|
|
253
|
+
except httpx2.HTTPStatusError as e:
|
|
254
|
+
session['oauth_error'] = e.response.text
|
|
255
|
+
return RedirectResponse(self.error_path, status_code=303)
|
|
253
256
|
ident = info.get(self.cli.id_key)
|
|
254
257
|
if not ident: return self.redir_login(session)
|
|
255
258
|
res = await _arun(self.get_auth(info, ident, session, state))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
@@ -8,7 +8,7 @@ dynamic = ["version"]
|
|
|
8
8
|
description = "The fastest way to create an HTML app"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
-
license =
|
|
11
|
+
license = "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"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.14
|
|
4
4
|
Summary: The fastest way to create an HTML app
|
|
5
5
|
Author-email: Jeremy Howard and contributors <github@jhoward.fastmail.fm>
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Repository, https://github.com/AnswerDotAI/fasthtml
|
|
8
8
|
Project-URL: Documentation, https://www.fastht.ml/docs/
|
|
9
9
|
Keywords: nbdev,jupyter,notebook,python
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{python_fasthtml-0.14.12 → python_fasthtml-0.14.14}/python_fasthtml.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|