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.
Files changed (39) hide show
  1. {python_fasthtml-0.14.2/python_fasthtml.egg-info → python_fasthtml-0.14.4}/PKG-INFO +2 -2
  2. python_fasthtml-0.14.4/fasthtml/__init__.py +2 -0
  3. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/_modidx.py +2 -1
  4. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/core.py +26 -21
  5. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/jupyter.py +20 -14
  6. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/oauth.py +7 -7
  7. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/xtend.py +8 -1
  8. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/pyproject.toml +1 -1
  9. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4/python_fasthtml.egg-info}/PKG-INFO +2 -2
  10. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/requires.txt +1 -1
  11. python_fasthtml-0.14.2/fasthtml/__init__.py +0 -2
  12. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/CONTRIBUTING.md +0 -0
  13. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/LICENSE +0 -0
  14. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/MANIFEST.in +0 -0
  15. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/README.md +0 -0
  16. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/authmw.py +0 -0
  17. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/basics.py +0 -0
  18. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/cli.py +0 -0
  19. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/common.py +0 -0
  20. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/components.py +0 -0
  21. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/components.pyi +0 -0
  22. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/core.pyi +0 -0
  23. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/fastapp.py +0 -0
  24. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/ft.py +0 -0
  25. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/js.py +0 -0
  26. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/katex.js +0 -0
  27. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/live_reload.py +0 -0
  28. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/pico.py +0 -0
  29. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/starlette.py +0 -0
  30. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/stripe_otp.py +0 -0
  31. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/svg.py +0 -0
  32. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/toaster.py +0 -0
  33. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/fasthtml/xtend.pyi +0 -0
  34. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/SOURCES.txt +0 -0
  35. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  36. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/entry_points.txt +0 -0
  37. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/python_fasthtml.egg-info/top_level.txt +0 -0
  38. {python_fasthtml-0.14.2 → python_fasthtml-0.14.4}/setup.cfg +0 -0
  39. {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.2
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: httpx
24
+ Requires-Dist: httpx2
25
25
  Requires-Dist: fastlite>=0.1.1
26
26
  Requires-Dist: python-multipart
27
27
  Requires-Dist: beautifulsoup4
@@ -0,0 +1,2 @@
1
+ __version__ = "0.14.4"
2
+ from .core import *
@@ -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', 'viewport',
9
- 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date', 'snake2hyphens',
10
- 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn', 'JSONResponse',
11
- 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple', 'noop_body',
12
- 'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML', 'HostRoute',
13
- 'nested_name', 'serve', 'Client', 'RouteFuncs', 'APIRouter', 'cookie', 'reg_re_param', 'StaticNoCache',
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
- return await req.json() if ctype == 'application/json' else await req.form()
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 not resp: resp=''
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 httpx
822
- self.cli = httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url=url)
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.route(f"{prefix}{{fname:path}}.{{ext:{exts}}}")
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.route(f"{prefix}{{fname:path}}{ext}")
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 httpx
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 = httpx.AsyncClient() if asink else httpx.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', 'show', 'render_ft', 'htmx_config_port', 'JupyUvi',
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=3):
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: return print(f"Timeout")
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 #9134035e
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
- wait_port_free(self.port)
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, httpx, time, asyncio, logging
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 = httpx.post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret)
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 httpx.get(f"https://{self.domain}/.well-known/openid-configuration").raise_for_status().json()
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 = httpx.post(self.token_url, data=payload).raise_for_status()
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 httpx.get(self.info_url, headers=headers).json()
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 httpx.AsyncClient() as c:
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 httpx.AsyncClient() as c:
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', 'httpx', 'fastlite>=0.1.1', 'python-multipart', 'beautifulsoup4']
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.2
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: httpx
24
+ Requires-Dist: httpx2
25
25
  Requires-Dist: fastlite>=0.1.1
26
26
  Requires-Dist: python-multipart
27
27
  Requires-Dist: beautifulsoup4
@@ -4,7 +4,7 @@ starlette>=1.0.1
4
4
  oauthlib
5
5
  itsdangerous
6
6
  uvicorn[standard]>=0.30
7
- httpx
7
+ httpx2
8
8
  fastlite>=0.1.1
9
9
  python-multipart
10
10
  beautifulsoup4
@@ -1,2 +0,0 @@
1
- __version__ = "0.14.2"
2
- from .core import *