python-fasthtml 0.14.1__tar.gz → 0.14.3__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.1/python_fasthtml.egg-info → python_fasthtml-0.14.3}/PKG-INFO +3 -3
- python_fasthtml-0.14.3/fasthtml/__init__.py +2 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/_modidx.py +3 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/components.py +3 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/core.py +23 -17
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/js.py +3 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/jupyter.py +46 -21
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/oauth.py +10 -8
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/pico.py +3 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/stripe_otp.py +2 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/svg.py +3 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/xtend.py +3 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/pyproject.toml +1 -1
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3/python_fasthtml.egg-info}/PKG-INFO +3 -3
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/python_fasthtml.egg-info/requires.txt +2 -2
- python_fasthtml-0.14.1/fasthtml/__init__.py +0 -2
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/CONTRIBUTING.md +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/LICENSE +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/MANIFEST.in +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/README.md +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/basics.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/cli.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/common.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/components.pyi +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/core.pyi +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/fastapp.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/ft.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/katex.js +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/fasthtml/xtend.pyi +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/python_fasthtml.egg-info/SOURCES.txt +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/setup.cfg +0 -0
- {python_fasthtml-0.14.1 → python_fasthtml-0.14.3}/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.3
|
|
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
|
|
@@ -17,11 +17,11 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: fastcore>=1.12.45
|
|
19
19
|
Requires-Dist: python-dateutil
|
|
20
|
-
Requires-Dist: starlette
|
|
20
|
+
Requires-Dist: starlette>=1.0.1
|
|
21
21
|
Requires-Dist: oauthlib
|
|
22
22
|
Requires-Dist: itsdangerous
|
|
23
23
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: httpx2
|
|
25
25
|
Requires-Dist: fastlite>=0.1.1
|
|
26
26
|
Requires-Dist: python-multipart
|
|
27
27
|
Requires-Dist: beautifulsoup4
|
|
@@ -173,8 +173,9 @@ d = { 'settings': { 'branch': 'main',
|
|
|
173
173
|
'fasthtml.jupyter': { 'fasthtml.jupyter.HTMX': ('api/jupyter.html#htmx', 'fasthtml/jupyter.py'),
|
|
174
174
|
'fasthtml.jupyter.JupyUvi': ('api/jupyter.html#jupyuvi', 'fasthtml/jupyter.py'),
|
|
175
175
|
'fasthtml.jupyter.JupyUvi.__init__': ('api/jupyter.html#jupyuvi.__init__', 'fasthtml/jupyter.py'),
|
|
176
|
+
'fasthtml.jupyter.JupyUvi._live_sse': ('api/jupyter.html#jupyuvi._live_sse', 'fasthtml/jupyter.py'),
|
|
177
|
+
'fasthtml.jupyter.JupyUvi._setup_live': ('api/jupyter.html#jupyuvi._setup_live', 'fasthtml/jupyter.py'),
|
|
176
178
|
'fasthtml.jupyter.JupyUvi.start': ('api/jupyter.html#jupyuvi.start', 'fasthtml/jupyter.py'),
|
|
177
|
-
'fasthtml.jupyter.JupyUvi.start_async': ('api/jupyter.html#jupyuvi.start_async', 'fasthtml/jupyter.py'),
|
|
178
179
|
'fasthtml.jupyter.JupyUvi.stop': ('api/jupyter.html#jupyuvi.stop', 'fasthtml/jupyter.py'),
|
|
179
180
|
'fasthtml.jupyter.JupyUviAsync': ('api/jupyter.html#jupyuviasync', 'fasthtml/jupyter.py'),
|
|
180
181
|
'fasthtml.jupyter.JupyUviAsync.__init__': ( 'api/jupyter.html#jupyuviasync.__init__',
|
|
@@ -188,6 +189,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
188
189
|
'fasthtml.jupyter.render_ft': ('api/jupyter.html#render_ft', 'fasthtml/jupyter.py'),
|
|
189
190
|
'fasthtml.jupyter.show': ('api/jupyter.html#show', 'fasthtml/jupyter.py'),
|
|
190
191
|
'fasthtml.jupyter.wait_port_free': ('api/jupyter.html#wait_port_free', 'fasthtml/jupyter.py'),
|
|
192
|
+
'fasthtml.jupyter.wait_port_free_async': ('api/jupyter.html#wait_port_free_async', 'fasthtml/jupyter.py'),
|
|
191
193
|
'fasthtml.jupyter.ws_client': ('api/jupyter.html#ws_client', 'fasthtml/jupyter.py')},
|
|
192
194
|
'fasthtml.live_reload': {},
|
|
193
195
|
'fasthtml.oauth': { 'fasthtml.oauth.AppleAppClient': ('api/oauth.html#appleappclient', 'fasthtml/oauth.py'),
|
|
@@ -1,4 +1,6 @@
|
|
|
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
|
|
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
|
+
|
|
3
|
+
Docs: https://www.fastht.ml/docsapi/components.html.md"""
|
|
2
4
|
|
|
3
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/01_components.ipynb.
|
|
4
6
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
"""The `FastHTML` subclass of `Starlette`.
|
|
1
|
+
"""The `FastHTML` subclass of `Starlette`.
|
|
2
|
+
|
|
3
|
+
Docs: https://www.fastht.ml/docsapi/core.html.md"""
|
|
2
4
|
|
|
3
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.
|
|
4
6
|
|
|
5
7
|
# %% auto #0
|
|
6
|
-
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc',
|
|
7
|
-
'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
|
|
8
|
-
'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn',
|
|
9
|
-
'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple',
|
|
10
|
-
'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML',
|
|
11
|
-
'nested_name', 'serve', 'Client', 'RouteFuncs', 'APIRouter', 'cookie', 'reg_re_param',
|
|
12
|
-
'add_sig_param', 'into', 'MiddlewareBase', 'FtResponse', 'unqid']
|
|
8
|
+
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'DEF_MAXPART', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc',
|
|
9
|
+
'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
|
|
10
|
+
'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn',
|
|
11
|
+
'JSONResponse', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple',
|
|
12
|
+
'noop_body', 'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML',
|
|
13
|
+
'HostRoute', 'nested_name', 'serve', 'Client', 'RouteFuncs', 'APIRouter', 'cookie', 'reg_re_param',
|
|
14
|
+
'StaticNoCache', 'add_sig_param', 'into', 'MiddlewareBase', 'FtResponse', 'unqid']
|
|
13
15
|
|
|
14
16
|
# %% ../nbs/api/00_core.ipynb #23503b9e
|
|
15
17
|
import json,uuid,inspect,types,asyncio,inspect,random,contextlib,itsdangerous
|
|
@@ -150,18 +152,21 @@ def form2dict(form: FormData) -> dict:
|
|
|
150
152
|
if isinstance(form, dict): return form
|
|
151
153
|
return {k: _formitem(form, k) for k in form}
|
|
152
154
|
|
|
155
|
+
# %% ../nbs/api/00_core.ipynb #8899bc0a
|
|
156
|
+
DEF_MAXPART = 100*1024*1024
|
|
157
|
+
|
|
153
158
|
# %% ../nbs/api/00_core.ipynb #42c9cea0
|
|
154
159
|
async def parse_form(req: Request) -> FormData:
|
|
155
160
|
"Starlette errors on empty multipart forms, so this checks for that situation"
|
|
156
161
|
ctype = req.headers.get("Content-Type", "")
|
|
162
|
+
maxpart = getattr(req, "max_part_size", DEF_MAXPART)
|
|
157
163
|
if ctype.startswith("multipart/form-data"):
|
|
158
164
|
try: boundary = ctype.split("boundary=")[1].strip()
|
|
159
165
|
except IndexError: raise HTTPException(400, "Invalid form-data: no boundary")
|
|
160
166
|
if int(req.headers.get("Content-Length", "0")) <= len(boundary) + 6: return FormData()
|
|
161
|
-
return await req.form()
|
|
167
|
+
return await req.form(max_part_size=maxpart)
|
|
162
168
|
await req.body() # Cache body for non-multipart request types
|
|
163
|
-
return await req.json() if ctype == 'application/json' else await req.form()
|
|
164
|
-
|
|
169
|
+
return await req.json() if ctype == 'application/json' else await req.form(max_part_size=maxpart)
|
|
165
170
|
|
|
166
171
|
# %% ../nbs/api/00_core.ipynb #0caedd04
|
|
167
172
|
async def _from_body(conn, p, data):
|
|
@@ -600,7 +605,7 @@ class FastHTML(Starlette):
|
|
|
600
605
|
before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
|
|
601
606
|
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
602
607
|
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
|
|
603
|
-
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, **bodykw):
|
|
608
|
+
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, max_part_size=DEF_MAXPART, **bodykw):
|
|
604
609
|
middleware,before,after = map(_list, (middleware,before,after))
|
|
605
610
|
self.title,self.canonical,self.session_cookie,self.key_fname = title,canonical,session_cookie,key_fname
|
|
606
611
|
hdrs,ftrs,exts = map(listify, (hdrs,ftrs,exts))
|
|
@@ -615,7 +620,7 @@ class FastHTML(Starlette):
|
|
|
615
620
|
middleware.append(cors_allow)
|
|
616
621
|
self.lifespan = Lifespan(on_startup, on_shutdown, lifespan)
|
|
617
622
|
self.hdrs,self.ftrs = hdrs,ftrs
|
|
618
|
-
self.body_wrap,self.before,self.after,self.htmlkw,self.bodykw = body_wrap,before,after,htmlkw,bodykw
|
|
623
|
+
self.body_wrap,self.before,self.after,self.htmlkw,self.bodykw,self.max_part_size = body_wrap,before,after,htmlkw,bodykw,max_part_size
|
|
619
624
|
self.secret_key = get_key(secret_key, key_fname)
|
|
620
625
|
if sess_cls:
|
|
621
626
|
sess = Middleware(sess_cls, secret_key=self.secret_key,session_cookie=session_cookie,
|
|
@@ -673,6 +678,7 @@ def _endp(self:FastHTML, f, body_wrap, before:Optional[Callable|tuple]=None):
|
|
|
673
678
|
async def _f(req):
|
|
674
679
|
resp = None
|
|
675
680
|
req.injects = []
|
|
681
|
+
req.max_part_size = self.max_part_size
|
|
676
682
|
req.hdrs,req.ftrs,req.htmlkw,req.bodykw = map(deepcopy, (self.hdrs,self.ftrs,self.htmlkw,self.bodykw))
|
|
677
683
|
req.hdrs,req.ftrs = listify(req.hdrs),listify(req.ftrs)
|
|
678
684
|
for b in self.before:
|
|
@@ -816,8 +822,8 @@ def serve(
|
|
|
816
822
|
class Client:
|
|
817
823
|
"A simple httpx ASGI client that doesn't require `async`"
|
|
818
824
|
def __init__(self, app, url="http://testserver"):
|
|
819
|
-
import
|
|
820
|
-
self.cli =
|
|
825
|
+
import httpx2
|
|
826
|
+
self.cli = httpx2.AsyncClient(transport=httpx2.ASGITransport(app), base_url=url)
|
|
821
827
|
|
|
822
828
|
def _sync(self, method, url, **kwargs):
|
|
823
829
|
async def _request(): return await self.cli.request(method, url, **kwargs)
|
|
@@ -1026,11 +1032,11 @@ def devtools_json(self:FastHTML, path=None, uuid=None):
|
|
|
1026
1032
|
@patch
|
|
1027
1033
|
def get_client(self:FastHTML, asink=False, **kw):
|
|
1028
1034
|
"Get an httpx client with session cookes set from `**kw`"
|
|
1029
|
-
import
|
|
1035
|
+
import httpx2
|
|
1030
1036
|
signer = itsdangerous.TimestampSigner(self.secret_key)
|
|
1031
1037
|
data = b64encode(dumps(kw).encode())
|
|
1032
1038
|
data = signer.sign(data)
|
|
1033
|
-
client =
|
|
1039
|
+
client = httpx2.AsyncClient() if asink else httpx2.Client()
|
|
1034
1040
|
client.cookies.update({self.session_cookie: data.decode()})
|
|
1035
1041
|
return client
|
|
1036
1042
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
"""Use FastHTML in Jupyter notebooks
|
|
1
|
+
"""Use FastHTML in Jupyter notebooks
|
|
2
|
+
|
|
3
|
+
Docs: https://www.fastht.ml/docsapi/jupyter.html.md"""
|
|
2
4
|
|
|
3
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/06_jupyter.ipynb.
|
|
4
6
|
|
|
5
7
|
# %% auto #0
|
|
6
|
-
__all__ = ['nb_serve', 'nb_serve_async', 'is_port_free', 'wait_port_free', '
|
|
7
|
-
'JupyUviAsync', 'HTMX', 'ws_client']
|
|
8
|
+
__all__ = ['nb_serve', 'nb_serve_async', 'is_port_free', 'wait_port_free', 'wait_port_free_async', 'show', 'render_ft',
|
|
9
|
+
'htmx_config_port', 'JupyUvi', 'JupyUviAsync', 'HTMX', 'ws_client']
|
|
8
10
|
|
|
9
11
|
# %% ../nbs/api/06_jupyter.ipynb #2c69d9d0
|
|
10
12
|
import asyncio, socket, time, uvicorn
|
|
@@ -36,23 +38,31 @@ async def nb_serve_async(app, log_level="error", port=8000, host='0.0.0.0', **kw
|
|
|
36
38
|
|
|
37
39
|
# %% ../nbs/api/06_jupyter.ipynb #508917bc
|
|
38
40
|
def is_port_free(port, host='localhost'):
|
|
39
|
-
"Check if `port` is free on `host`"
|
|
40
41
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
42
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
41
43
|
try:
|
|
42
|
-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
43
44
|
sock.bind((host, port))
|
|
45
|
+
sock.listen(1)
|
|
44
46
|
return True
|
|
45
47
|
except OSError: return False
|
|
46
48
|
finally: sock.close()
|
|
47
49
|
|
|
48
50
|
# %% ../nbs/api/06_jupyter.ipynb #1779cb76
|
|
49
|
-
def wait_port_free(port, host='localhost', max_wait=
|
|
51
|
+
def wait_port_free(port, host='localhost', max_wait=20):
|
|
50
52
|
"Wait for `port` to be free on `host`"
|
|
51
53
|
start_time = time.time()
|
|
52
|
-
while not is_port_free(port):
|
|
53
|
-
if time.time() - start_time>max_wait:
|
|
54
|
+
while not is_port_free(port, host):
|
|
55
|
+
if time.time() - start_time > max_wait: raise TimeoutError(f"Port {host}:{port} not free after {max_wait}s")
|
|
54
56
|
time.sleep(0.1)
|
|
55
57
|
|
|
58
|
+
async def wait_port_free_async(port, host='localhost', max_wait=20):
|
|
59
|
+
"Async wait for `port` to be free on `host`"
|
|
60
|
+
start_time = time.time()
|
|
61
|
+
while not is_port_free(port, host):
|
|
62
|
+
if time.time() - start_time > max_wait: raise TimeoutError(f"Port {host}:{port} not free after {max_wait}s")
|
|
63
|
+
await asyncio.sleep(0.1)
|
|
64
|
+
|
|
65
|
+
|
|
56
66
|
# %% ../nbs/api/06_jupyter.ipynb #654b36bb
|
|
57
67
|
@delegates(_show)
|
|
58
68
|
def show(*s, **kwargs):
|
|
@@ -79,28 +89,42 @@ document.body.addEventListener('htmx:configRequest', (event) => {
|
|
|
79
89
|
});
|
|
80
90
|
</script>''' % port))
|
|
81
91
|
|
|
82
|
-
# %% ../nbs/api/06_jupyter.ipynb #
|
|
92
|
+
# %% ../nbs/api/06_jupyter.ipynb #79406618
|
|
83
93
|
class JupyUvi:
|
|
84
94
|
"Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
|
|
85
|
-
def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True, daemon=False, **kwargs):
|
|
95
|
+
def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True, live=False, live_rt='/_lr', daemon=False, **kwargs):
|
|
86
96
|
self.kwargs = kwargs
|
|
87
|
-
store_attr(but='start')
|
|
97
|
+
store_attr(but='start,live')
|
|
88
98
|
self.server = None
|
|
99
|
+
self._live_ver = 0
|
|
100
|
+
if live: self._setup_live(app)
|
|
89
101
|
if start: self.start()
|
|
90
102
|
if not os.environ.get('IN_SOLVEIT'): htmx_config_port(port)
|
|
91
|
-
|
|
103
|
+
|
|
92
104
|
def start(self):
|
|
93
105
|
self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs)
|
|
94
106
|
|
|
95
|
-
async def start_async(self):
|
|
96
|
-
self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)
|
|
97
|
-
|
|
98
107
|
def stop(self):
|
|
99
108
|
self.server.should_exit = True
|
|
100
|
-
wait_port_free(self.port)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
wait_port_free(self.port, self.host)
|
|
110
|
+
|
|
111
|
+
def _setup_live(self, app):
|
|
112
|
+
rt = self.live_rt or '/_lr'
|
|
113
|
+
if not rt.startswith('/'): rt = f'/{rt}'
|
|
114
|
+
app.hdrs.append(Script(f"new EventSource({rt!r}).onmessage=e=>{{if(e.data==='reload')navigation.reload()}}"))
|
|
115
|
+
@app.get(rt)
|
|
116
|
+
async def _sse(): return EventStream(self._live_sse())
|
|
117
|
+
get_ipython().events.register('post_run_cell', lambda _: setattr(self, '_live_ver', self._live_ver+1))
|
|
118
|
+
|
|
119
|
+
async def _live_sse(self):
|
|
120
|
+
ver = self._live_ver
|
|
121
|
+
while not self.server.should_exit:
|
|
122
|
+
await asyncio.sleep(0.1)
|
|
123
|
+
if ver != self._live_ver:
|
|
124
|
+
ver = self._live_ver
|
|
125
|
+
yield 'data: reload\n\n'
|
|
126
|
+
|
|
127
|
+
# %% ../nbs/api/06_jupyter.ipynb #f6316c73
|
|
104
128
|
class JupyUviAsync(JupyUvi):
|
|
105
129
|
"Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
|
|
106
130
|
def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, **kwargs):
|
|
@@ -109,9 +133,10 @@ class JupyUviAsync(JupyUvi):
|
|
|
109
133
|
async def start(self):
|
|
110
134
|
self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)
|
|
111
135
|
|
|
112
|
-
def stop(self):
|
|
136
|
+
async def stop(self):
|
|
113
137
|
self.server.should_exit = True
|
|
114
|
-
|
|
138
|
+
await wait_port_free_async(self.port, self.host)
|
|
139
|
+
|
|
115
140
|
|
|
116
141
|
# %% ../nbs/api/06_jupyter.ipynb #a448e420
|
|
117
142
|
from starlette.testclient import TestClient
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"""Basic scaffolding for handling OAuth
|
|
1
|
+
"""Basic scaffolding for handling OAuth
|
|
2
|
+
|
|
3
|
+
Docs: https://www.fastht.ml/docsapi/oauth.html.md"""
|
|
2
4
|
|
|
3
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/08_oauth.ipynb.
|
|
4
6
|
|
|
@@ -10,7 +12,7 @@ __all__ = ['log', 'http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'Huggin
|
|
|
10
12
|
from .common import *
|
|
11
13
|
from oauthlib.oauth2 import WebApplicationClient
|
|
12
14
|
from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote
|
|
13
|
-
import secrets,
|
|
15
|
+
import secrets, httpx2, time, asyncio, logging
|
|
14
16
|
|
|
15
17
|
# %% ../nbs/api/08_oauth.ipynb #44aa4a88
|
|
16
18
|
log = logging.getLogger(__name__)
|
|
@@ -93,7 +95,7 @@ class DiscordAppClient(_AppClient):
|
|
|
93
95
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
94
96
|
data = dict(grant_type='authorization_code', code=code)
|
|
95
97
|
if redirect_uri: data['redirect_uri'] = redirect_uri
|
|
96
|
-
r =
|
|
98
|
+
r = httpx2.post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret)
|
|
97
99
|
).raise_for_status()
|
|
98
100
|
self.parse_request_body_response(r.text)
|
|
99
101
|
|
|
@@ -107,7 +109,7 @@ class Auth0AppClient(_AppClient):
|
|
|
107
109
|
super().__init__(client_id, client_secret, code=code, scope=scope, redirect_uri=redirect_uri, **kwargs)
|
|
108
110
|
|
|
109
111
|
def _fetch_openid_config(self):
|
|
110
|
-
return
|
|
112
|
+
return httpx2.get(f"https://{self.domain}/.well-known/openid-configuration").raise_for_status().json()
|
|
111
113
|
|
|
112
114
|
def login_link(self, req):
|
|
113
115
|
d = dict(response_type="code", client_id=self.client_id, scope=self.scope, redirect_uri=redir_url(req, self.redirect_uri))
|
|
@@ -167,7 +169,7 @@ def parse_response(self:_AppClient, code, redirect_uri):
|
|
|
167
169
|
"Get the token from the oauth2 server response"
|
|
168
170
|
payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,
|
|
169
171
|
client_secret=self.client_secret, grant_type='authorization_code')
|
|
170
|
-
r =
|
|
172
|
+
r = httpx2.post(self.token_url, data=payload).raise_for_status()
|
|
171
173
|
self.parse_request_body_response(r.text)
|
|
172
174
|
|
|
173
175
|
@patch
|
|
@@ -175,7 +177,7 @@ def get_info(self:_AppClient, token=None):
|
|
|
175
177
|
"Get the info for authenticated user"
|
|
176
178
|
if not token: token = self.token["access_token"]
|
|
177
179
|
headers = {'Authorization': f'Bearer {token}'}
|
|
178
|
-
return
|
|
180
|
+
return httpx2.get(self.info_url, headers=headers).json()
|
|
179
181
|
|
|
180
182
|
@patch
|
|
181
183
|
def retr_info(self:_AppClient, code, redirect_uri):
|
|
@@ -190,7 +192,7 @@ async def parse_response_async(self:_AppClient, code, redirect_uri):
|
|
|
190
192
|
payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,
|
|
191
193
|
client_secret=self.client_secret, grant_type='authorization_code')
|
|
192
194
|
log.debug(f"OAuth token request: redirect_uri={redirect_uri}, code={code[:20]}...")
|
|
193
|
-
async with
|
|
195
|
+
async with httpx2.AsyncClient() as c:
|
|
194
196
|
r = (await c.post(self.token_url, data=payload))
|
|
195
197
|
log.debug(f"OAuth response: {r.status_code} - {r.text}")
|
|
196
198
|
r.raise_for_status()
|
|
@@ -201,7 +203,7 @@ async def get_info_async(self:_AppClient, token=None):
|
|
|
201
203
|
"Get the info for authenticated user"
|
|
202
204
|
if not token: token = self.token["access_token"]
|
|
203
205
|
headers = {'Authorization': f'Bearer {token}'}
|
|
204
|
-
async with
|
|
206
|
+
async with httpx2.AsyncClient() as c:
|
|
205
207
|
return (await c.get(self.info_url, headers=headers)).raise_for_status().json()
|
|
206
208
|
|
|
207
209
|
@patch
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"""Simple extensions to standard HTML components, such as adding sensible defaults
|
|
1
|
+
"""Simple extensions to standard HTML components, such as adding sensible defaults
|
|
2
|
+
|
|
3
|
+
Docs: https://www.fastht.ml/docsapi/xtend.html.md"""
|
|
2
4
|
|
|
3
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/02_xtend.ipynb.
|
|
4
6
|
|
|
@@ -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.45', 'python-dateutil', 'starlette
|
|
15
|
+
dependencies = ['fastcore>=1.12.45', 'python-dateutil', 'starlette>=1.0.1', 'oauthlib', 'itsdangerous', 'uvicorn[standard]>=0.30', 'httpx2', 'fastlite>=0.1.1', 'python-multipart', 'beautifulsoup4']
|
|
16
16
|
|
|
17
17
|
[project.urls]
|
|
18
18
|
Repository = "https://github.com/AnswerDotAI/fasthtml"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.3
|
|
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
|
|
@@ -17,11 +17,11 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: fastcore>=1.12.45
|
|
19
19
|
Requires-Dist: python-dateutil
|
|
20
|
-
Requires-Dist: starlette
|
|
20
|
+
Requires-Dist: starlette>=1.0.1
|
|
21
21
|
Requires-Dist: oauthlib
|
|
22
22
|
Requires-Dist: itsdangerous
|
|
23
23
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: httpx2
|
|
25
25
|
Requires-Dist: fastlite>=0.1.1
|
|
26
26
|
Requires-Dist: python-multipart
|
|
27
27
|
Requires-Dist: beautifulsoup4
|
|
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.1 → python_fasthtml-0.14.3}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|