python-fasthtml 0.14.2__tar.gz → 0.14.4__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.2/python_fasthtml.egg-info → python_fasthtml-0.14.4}/PKG-INFO +2 -2
- python_fasthtml-0.14.4/fasthtml/__init__.py +2 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/_modidx.py +2 -1
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/core.py +26 -21
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/jupyter.py +20 -14
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/oauth.py +7 -7
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/xtend.py +8 -1
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/pyproject.toml +1 -1
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4/python_fasthtml.egg-info}/PKG-INFO +2 -2
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/requires.txt +1 -1
- python_fasthtml-0.14.2/fasthtml/__init__.py +0 -2
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/CONTRIBUTING.md +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/LICENSE +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/MANIFEST.in +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/README.md +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/authmw.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/basics.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/cli.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/common.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/components.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/components.pyi +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/core.pyi +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/fastapp.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/ft.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/js.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/katex.js +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/live_reload.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/pico.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/starlette.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/stripe_otp.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/svg.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/toaster.py +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/xtend.pyi +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/SOURCES.txt +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/setup.cfg +0 -0
- {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/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.4
|
|
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
|
|
@@ -21,7 +21,7 @@ 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
|
|
@@ -176,7 +176,6 @@ d = { 'settings': { 'branch': 'main',
|
|
|
176
176
|
'fasthtml.jupyter.JupyUvi._live_sse': ('api/jupyter.html#jupyuvi._live_sse', 'fasthtml/jupyter.py'),
|
|
177
177
|
'fasthtml.jupyter.JupyUvi._setup_live': ('api/jupyter.html#jupyuvi._setup_live', 'fasthtml/jupyter.py'),
|
|
178
178
|
'fasthtml.jupyter.JupyUvi.start': ('api/jupyter.html#jupyuvi.start', 'fasthtml/jupyter.py'),
|
|
179
|
-
'fasthtml.jupyter.JupyUvi.start_async': ('api/jupyter.html#jupyuvi.start_async', 'fasthtml/jupyter.py'),
|
|
180
179
|
'fasthtml.jupyter.JupyUvi.stop': ('api/jupyter.html#jupyuvi.stop', 'fasthtml/jupyter.py'),
|
|
181
180
|
'fasthtml.jupyter.JupyUviAsync': ('api/jupyter.html#jupyuviasync', 'fasthtml/jupyter.py'),
|
|
182
181
|
'fasthtml.jupyter.JupyUviAsync.__init__': ( 'api/jupyter.html#jupyuviasync.__init__',
|
|
@@ -190,6 +189,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
190
189
|
'fasthtml.jupyter.render_ft': ('api/jupyter.html#render_ft', 'fasthtml/jupyter.py'),
|
|
191
190
|
'fasthtml.jupyter.show': ('api/jupyter.html#show', 'fasthtml/jupyter.py'),
|
|
192
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'),
|
|
193
193
|
'fasthtml.jupyter.ws_client': ('api/jupyter.html#ws_client', 'fasthtml/jupyter.py')},
|
|
194
194
|
'fasthtml.live_reload': {},
|
|
195
195
|
'fasthtml.oauth': { 'fasthtml.oauth.AppleAppClient': ('api/oauth.html#appleappclient', 'fasthtml/oauth.py'),
|
|
@@ -339,4 +339,5 @@ d = { 'settings': { 'branch': 'main',
|
|
|
339
339
|
'fasthtml.xtend.sitemap_url': ('api/xtend.html#sitemap_url', 'fasthtml/xtend.py'),
|
|
340
340
|
'fasthtml.xtend.sitemap_xml': ('api/xtend.html#sitemap_xml', 'fasthtml/xtend.py'),
|
|
341
341
|
'fasthtml.xtend.undouble_braces': ('api/xtend.html#undouble_braces', 'fasthtml/xtend.py'),
|
|
342
|
+
'fasthtml.xtend.upload_pb_attrs': ('api/xtend.html#upload_pb_attrs', 'fasthtml/xtend.py'),
|
|
342
343
|
'fasthtml.xtend.with_sid': ('api/xtend.html#with_sid', 'fasthtml/xtend.py')}}}
|
|
@@ -5,13 +5,13 @@ Docs: https://www.fastht.ml/docsapi/core.html.md"""
|
|
|
5
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.
|
|
6
6
|
|
|
7
7
|
# %% auto #0
|
|
8
|
-
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc',
|
|
9
|
-
'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
|
|
10
|
-
'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn',
|
|
11
|
-
'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple',
|
|
12
|
-
'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML',
|
|
13
|
-
'nested_name', 'serve', 'Client', 'RouteFuncs', 'APIRouter', 'cookie', 'reg_re_param',
|
|
14
|
-
'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']
|
|
15
15
|
|
|
16
16
|
# %% ../nbs/api/00_core.ipynb #23503b9e
|
|
17
17
|
import json,uuid,inspect,types,asyncio,inspect,random,contextlib,itsdangerous
|
|
@@ -152,18 +152,22 @@ def form2dict(form: FormData) -> dict:
|
|
|
152
152
|
if isinstance(form, dict): return form
|
|
153
153
|
return {k: _formitem(form, k) for k in form}
|
|
154
154
|
|
|
155
|
+
# %% ../nbs/api/00_core.ipynb #8899bc0a
|
|
156
|
+
DEF_MAXPART = 100*1024*1024
|
|
157
|
+
|
|
155
158
|
# %% ../nbs/api/00_core.ipynb #42c9cea0
|
|
156
159
|
async def parse_form(req: Request) -> FormData:
|
|
157
|
-
"Starlette errors on empty multipart forms, so this checks for that situation"
|
|
160
|
+
"Starlette errors on empty multipart/json forms, so this checks for that situation"
|
|
158
161
|
ctype = req.headers.get("Content-Type", "")
|
|
162
|
+
maxpart = getattr(req, "max_part_size", DEF_MAXPART)
|
|
159
163
|
if ctype.startswith("multipart/form-data"):
|
|
160
164
|
try: boundary = ctype.split("boundary=")[1].strip()
|
|
161
165
|
except IndexError: raise HTTPException(400, "Invalid form-data: no boundary")
|
|
162
166
|
if int(req.headers.get("Content-Length", "0")) <= len(boundary) + 6: return FormData()
|
|
163
|
-
return await req.form()
|
|
164
|
-
await req.body() # Cache body for non-multipart request types
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
return await req.form(max_part_size=maxpart)
|
|
168
|
+
body = await req.body() # Cache body for non-multipart request types
|
|
169
|
+
if ctype == 'application/json': return await req.json() if body else {}
|
|
170
|
+
return await req.form(max_part_size=maxpart)
|
|
167
171
|
|
|
168
172
|
# %% ../nbs/api/00_core.ipynb #0caedd04
|
|
169
173
|
async def _from_body(conn, p, data):
|
|
@@ -455,7 +459,7 @@ def _is_ft_resp(resp):
|
|
|
455
459
|
# %% ../nbs/api/00_core.ipynb #968d9245
|
|
456
460
|
def _resp(req, resp, cls=empty, status_code=200):
|
|
457
461
|
"Create appropriate HTTP response from request and response data"
|
|
458
|
-
if
|
|
462
|
+
if resp is None: resp=''
|
|
459
463
|
if hasattr(resp, '__response__'): resp = resp.__response__(req)
|
|
460
464
|
if not (isinstance(cls, type) and issubclass(cls, Response)): cls=empty
|
|
461
465
|
if isinstance(resp, FileResponse) and not os.path.exists(resp.path): raise HTTPException(404, resp.path)
|
|
@@ -602,7 +606,7 @@ class FastHTML(Starlette):
|
|
|
602
606
|
before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
|
|
603
607
|
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
604
608
|
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
|
|
605
|
-
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, **bodykw):
|
|
609
|
+
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, max_part_size=DEF_MAXPART, **bodykw):
|
|
606
610
|
middleware,before,after = map(_list, (middleware,before,after))
|
|
607
611
|
self.title,self.canonical,self.session_cookie,self.key_fname = title,canonical,session_cookie,key_fname
|
|
608
612
|
hdrs,ftrs,exts = map(listify, (hdrs,ftrs,exts))
|
|
@@ -617,7 +621,7 @@ class FastHTML(Starlette):
|
|
|
617
621
|
middleware.append(cors_allow)
|
|
618
622
|
self.lifespan = Lifespan(on_startup, on_shutdown, lifespan)
|
|
619
623
|
self.hdrs,self.ftrs = hdrs,ftrs
|
|
620
|
-
self.body_wrap,self.before,self.after,self.htmlkw,self.bodykw = body_wrap,before,after,htmlkw,bodykw
|
|
624
|
+
self.body_wrap,self.before,self.after,self.htmlkw,self.bodykw,self.max_part_size = body_wrap,before,after,htmlkw,bodykw,max_part_size
|
|
621
625
|
self.secret_key = get_key(secret_key, key_fname)
|
|
622
626
|
if sess_cls:
|
|
623
627
|
sess = Middleware(sess_cls, secret_key=self.secret_key,session_cookie=session_cookie,
|
|
@@ -675,6 +679,7 @@ def _endp(self:FastHTML, f, body_wrap, before:Optional[Callable|tuple]=None):
|
|
|
675
679
|
async def _f(req):
|
|
676
680
|
resp = None
|
|
677
681
|
req.injects = []
|
|
682
|
+
req.max_part_size = self.max_part_size
|
|
678
683
|
req.hdrs,req.ftrs,req.htmlkw,req.bodykw = map(deepcopy, (self.hdrs,self.ftrs,self.htmlkw,self.bodykw))
|
|
679
684
|
req.hdrs,req.ftrs = listify(req.hdrs),listify(req.ftrs)
|
|
680
685
|
for b in self.before:
|
|
@@ -818,8 +823,8 @@ def serve(
|
|
|
818
823
|
class Client:
|
|
819
824
|
"A simple httpx ASGI client that doesn't require `async`"
|
|
820
825
|
def __init__(self, app, url="http://testserver"):
|
|
821
|
-
import
|
|
822
|
-
self.cli =
|
|
826
|
+
import httpx2
|
|
827
|
+
self.cli = httpx2.AsyncClient(transport=httpx2.ASGITransport(app), base_url=url)
|
|
823
828
|
|
|
824
829
|
def _sync(self, method, url, **kwargs):
|
|
825
830
|
async def _request(): return await self.cli.request(method, url, **kwargs)
|
|
@@ -912,14 +917,14 @@ reg_re_param("static", '|'.join(_static_exts))
|
|
|
912
917
|
@patch
|
|
913
918
|
def static_route_exts(self:FastHTML, prefix='/', static_path='.', exts='static'):
|
|
914
919
|
"Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`"
|
|
915
|
-
@self.
|
|
920
|
+
@self.get(f"{prefix}{{fname:path}}.{{ext:{exts}}}")
|
|
916
921
|
async def get(fname:str, ext:str): return FileResponse(f'{static_path}/{fname}.{ext}')
|
|
917
922
|
|
|
918
923
|
# %% ../nbs/api/00_core.ipynb #b31de65a
|
|
919
924
|
@patch
|
|
920
925
|
def static_route(self:FastHTML, ext='', prefix='/', static_path='.'):
|
|
921
926
|
"Add a static route at URL path `prefix` with files from `static_path` and single `ext` (including the '.')"
|
|
922
|
-
@self.
|
|
927
|
+
@self.get(f"{prefix}{{fname:path}}{ext}")
|
|
923
928
|
async def get(fname:str): return FileResponse(f'{static_path}/{fname}{ext}')
|
|
924
929
|
|
|
925
930
|
# %% ../nbs/api/00_core.ipynb #f63b7a03
|
|
@@ -1028,11 +1033,11 @@ def devtools_json(self:FastHTML, path=None, uuid=None):
|
|
|
1028
1033
|
@patch
|
|
1029
1034
|
def get_client(self:FastHTML, asink=False, **kw):
|
|
1030
1035
|
"Get an httpx client with session cookes set from `**kw`"
|
|
1031
|
-
import
|
|
1036
|
+
import httpx2
|
|
1032
1037
|
signer = itsdangerous.TimestampSigner(self.secret_key)
|
|
1033
1038
|
data = b64encode(dumps(kw).encode())
|
|
1034
1039
|
data = signer.sign(data)
|
|
1035
|
-
client =
|
|
1040
|
+
client = httpx2.AsyncClient() if asink else httpx2.Client()
|
|
1036
1041
|
client.cookies.update({self.session_cookie: data.decode()})
|
|
1037
1042
|
return client
|
|
1038
1043
|
|
|
@@ -5,8 +5,8 @@ Docs: https://www.fastht.ml/docsapi/jupyter.html.md"""
|
|
|
5
5
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/06_jupyter.ipynb.
|
|
6
6
|
|
|
7
7
|
# %% auto #0
|
|
8
|
-
__all__ = ['nb_serve', 'nb_serve_async', 'is_port_free', 'wait_port_free', '
|
|
9
|
-
'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']
|
|
10
10
|
|
|
11
11
|
# %% ../nbs/api/06_jupyter.ipynb #2c69d9d0
|
|
12
12
|
import asyncio, socket, time, uvicorn
|
|
@@ -38,23 +38,31 @@ async def nb_serve_async(app, log_level="error", port=8000, host='0.0.0.0', **kw
|
|
|
38
38
|
|
|
39
39
|
# %% ../nbs/api/06_jupyter.ipynb #508917bc
|
|
40
40
|
def is_port_free(port, host='localhost'):
|
|
41
|
-
"Check if `port` is free on `host`"
|
|
42
41
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
42
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
43
43
|
try:
|
|
44
|
-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
45
44
|
sock.bind((host, port))
|
|
45
|
+
sock.listen(1)
|
|
46
46
|
return True
|
|
47
47
|
except OSError: return False
|
|
48
48
|
finally: sock.close()
|
|
49
49
|
|
|
50
50
|
# %% ../nbs/api/06_jupyter.ipynb #1779cb76
|
|
51
|
-
def wait_port_free(port, host='localhost', max_wait=
|
|
51
|
+
def wait_port_free(port, host='localhost', max_wait=20):
|
|
52
52
|
"Wait for `port` to be free on `host`"
|
|
53
53
|
start_time = time.time()
|
|
54
|
-
while not is_port_free(port):
|
|
55
|
-
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")
|
|
56
56
|
time.sleep(0.1)
|
|
57
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
|
+
|
|
58
66
|
# %% ../nbs/api/06_jupyter.ipynb #654b36bb
|
|
59
67
|
@delegates(_show)
|
|
60
68
|
def show(*s, **kwargs):
|
|
@@ -96,12 +104,9 @@ class JupyUvi:
|
|
|
96
104
|
def start(self):
|
|
97
105
|
self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs)
|
|
98
106
|
|
|
99
|
-
async def start_async(self):
|
|
100
|
-
self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)
|
|
101
|
-
|
|
102
107
|
def stop(self):
|
|
103
108
|
self.server.should_exit = True
|
|
104
|
-
wait_port_free(self.port)
|
|
109
|
+
wait_port_free(self.port, self.host)
|
|
105
110
|
|
|
106
111
|
def _setup_live(self, app):
|
|
107
112
|
rt = self.live_rt or '/_lr'
|
|
@@ -119,7 +124,7 @@ class JupyUvi:
|
|
|
119
124
|
ver = self._live_ver
|
|
120
125
|
yield 'data: reload\n\n'
|
|
121
126
|
|
|
122
|
-
# %% ../nbs/api/06_jupyter.ipynb #
|
|
127
|
+
# %% ../nbs/api/06_jupyter.ipynb #f6316c73
|
|
123
128
|
class JupyUviAsync(JupyUvi):
|
|
124
129
|
"Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
|
|
125
130
|
def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, **kwargs):
|
|
@@ -128,9 +133,10 @@ class JupyUviAsync(JupyUvi):
|
|
|
128
133
|
async def start(self):
|
|
129
134
|
self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)
|
|
130
135
|
|
|
131
|
-
def stop(self):
|
|
136
|
+
async def stop(self):
|
|
132
137
|
self.server.should_exit = True
|
|
133
|
-
|
|
138
|
+
await wait_port_free_async(self.port, self.host)
|
|
139
|
+
|
|
134
140
|
|
|
135
141
|
# %% ../nbs/api/06_jupyter.ipynb #a448e420
|
|
136
142
|
from starlette.testclient import TestClient
|
|
@@ -12,7 +12,7 @@ __all__ = ['log', 'http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'Huggin
|
|
|
12
12
|
from .common import *
|
|
13
13
|
from oauthlib.oauth2 import WebApplicationClient
|
|
14
14
|
from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote
|
|
15
|
-
import secrets,
|
|
15
|
+
import secrets, httpx2, time, asyncio, logging
|
|
16
16
|
|
|
17
17
|
# %% ../nbs/api/08_oauth.ipynb #44aa4a88
|
|
18
18
|
log = logging.getLogger(__name__)
|
|
@@ -95,7 +95,7 @@ class DiscordAppClient(_AppClient):
|
|
|
95
95
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
96
96
|
data = dict(grant_type='authorization_code', code=code)
|
|
97
97
|
if redirect_uri: data['redirect_uri'] = redirect_uri
|
|
98
|
-
r =
|
|
98
|
+
r = httpx2.post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret)
|
|
99
99
|
).raise_for_status()
|
|
100
100
|
self.parse_request_body_response(r.text)
|
|
101
101
|
|
|
@@ -109,7 +109,7 @@ class Auth0AppClient(_AppClient):
|
|
|
109
109
|
super().__init__(client_id, client_secret, code=code, scope=scope, redirect_uri=redirect_uri, **kwargs)
|
|
110
110
|
|
|
111
111
|
def _fetch_openid_config(self):
|
|
112
|
-
return
|
|
112
|
+
return httpx2.get(f"https://{self.domain}/.well-known/openid-configuration").raise_for_status().json()
|
|
113
113
|
|
|
114
114
|
def login_link(self, req):
|
|
115
115
|
d = dict(response_type="code", client_id=self.client_id, scope=self.scope, redirect_uri=redir_url(req, self.redirect_uri))
|
|
@@ -169,7 +169,7 @@ def parse_response(self:_AppClient, code, redirect_uri):
|
|
|
169
169
|
"Get the token from the oauth2 server response"
|
|
170
170
|
payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,
|
|
171
171
|
client_secret=self.client_secret, grant_type='authorization_code')
|
|
172
|
-
r =
|
|
172
|
+
r = httpx2.post(self.token_url, data=payload).raise_for_status()
|
|
173
173
|
self.parse_request_body_response(r.text)
|
|
174
174
|
|
|
175
175
|
@patch
|
|
@@ -177,7 +177,7 @@ def get_info(self:_AppClient, token=None):
|
|
|
177
177
|
"Get the info for authenticated user"
|
|
178
178
|
if not token: token = self.token["access_token"]
|
|
179
179
|
headers = {'Authorization': f'Bearer {token}'}
|
|
180
|
-
return
|
|
180
|
+
return httpx2.get(self.info_url, headers=headers).json()
|
|
181
181
|
|
|
182
182
|
@patch
|
|
183
183
|
def retr_info(self:_AppClient, code, redirect_uri):
|
|
@@ -192,7 +192,7 @@ async def parse_response_async(self:_AppClient, code, redirect_uri):
|
|
|
192
192
|
payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,
|
|
193
193
|
client_secret=self.client_secret, grant_type='authorization_code')
|
|
194
194
|
log.debug(f"OAuth token request: redirect_uri={redirect_uri}, code={code[:20]}...")
|
|
195
|
-
async with
|
|
195
|
+
async with httpx2.AsyncClient() as c:
|
|
196
196
|
r = (await c.post(self.token_url, data=payload))
|
|
197
197
|
log.debug(f"OAuth response: {r.status_code} - {r.text}")
|
|
198
198
|
r.raise_for_status()
|
|
@@ -203,7 +203,7 @@ async def get_info_async(self:_AppClient, token=None):
|
|
|
203
203
|
"Get the info for authenticated user"
|
|
204
204
|
if not token: token = self.token["access_token"]
|
|
205
205
|
headers = {'Authorization': f'Bearer {token}'}
|
|
206
|
-
async with
|
|
206
|
+
async with httpx2.AsyncClient() as c:
|
|
207
207
|
return (await c.get(self.info_url, headers=headers)).raise_for_status().json()
|
|
208
208
|
|
|
209
209
|
@patch
|
|
@@ -9,7 +9,7 @@ __all__ = ['sid_scr', 'A', 'AX', 'Form', 'Hidden', 'CheckboxX', 'Script', 'Style
|
|
|
9
9
|
'loose_format', 'ScriptX', 'replace_css_vars', 'StyleX', 'Nbsp', 'Surreal', 'On', 'Prev', 'Now', 'AnyNow',
|
|
10
10
|
'run_js', 'HtmxOn', 'jsd', 'Fragment', 'Titled', 'Socials', 'YouTubeEmbed', 'Favicon', 'clear', 'with_sid',
|
|
11
11
|
'LdJson', 'LdContactPoint', 'LdOrg', 'LdWebsite', 'LdCourseInstance', 'LdCourse', 'robots_txt',
|
|
12
|
-
'sitemap_url', 'sitemap_xml']
|
|
12
|
+
'sitemap_url', 'sitemap_xml', 'upload_pb_attrs']
|
|
13
13
|
|
|
14
14
|
# %% ../nbs/api/02_xtend.ipynb #8e2d405b
|
|
15
15
|
from dataclasses import dataclass, asdict
|
|
@@ -349,3 +349,10 @@ def sitemap_xml(app, urls, loc_base=""):
|
|
|
349
349
|
def get():
|
|
350
350
|
urlset = [sitemap_url(url_info, loc_base) for url_info in urls]
|
|
351
351
|
return Urlset(*urlset, xmlns="http://www.sitemaps.org/schemas/sitemap/0.9")
|
|
352
|
+
|
|
353
|
+
# %% ../nbs/api/02_xtend.ipynb #6a1990ed
|
|
354
|
+
def upload_pb_attrs(id='uploadprog'):
|
|
355
|
+
"Form `hx_on` progress attr that reveals and updates a hidden `upload_pb` of matching `id`"
|
|
356
|
+
js = """{ const p=document.getElementById('%s');
|
|
357
|
+
if(event.detail.total) {p.classList.remove('hidden'); p.value=event.detail.loaded/event.detail.total*100} }""" % id
|
|
358
|
+
return {'hx-on::xhr:progress': js}
|
|
@@ -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>=1.0.1', 'oauthlib', 'itsdangerous', 'uvicorn[standard]>=0.30', '
|
|
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.4
|
|
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
|
|
@@ -21,7 +21,7 @@ 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|