python-fasthtml 0.9.2__tar.gz → 0.10.1__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 (41) hide show
  1. {python-fasthtml-0.9.2/python_fasthtml.egg-info → python-fasthtml-0.10.1}/PKG-INFO +1 -1
  2. python-fasthtml-0.10.1/fasthtml/__init__.py +2 -0
  3. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/_modidx.py +7 -1
  4. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/components.py +22 -8
  5. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/live_reload.py +15 -25
  6. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/oauth.py +32 -13
  7. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/toaster.py +23 -9
  8. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1/python_fasthtml.egg-info}/PKG-INFO +1 -1
  9. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/settings.ini +1 -1
  10. python-fasthtml-0.9.2/fasthtml/__init__.py +0 -2
  11. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/CONTRIBUTING.md +0 -0
  12. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/LICENSE +0 -0
  13. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/MANIFEST.in +0 -0
  14. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/README.md +0 -0
  15. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/authmw.py +0 -0
  16. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/basics.py +0 -0
  17. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/cli.py +0 -0
  18. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/common.py +0 -0
  19. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/components.pyi +0 -0
  20. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/core.py +0 -0
  21. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/core.pyi +0 -0
  22. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/fastapp.py +0 -0
  23. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/ft.py +0 -0
  24. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/js.py +0 -0
  25. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/jupyter.py +0 -0
  26. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/katex.js +0 -0
  27. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/pico.py +0 -0
  28. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/starlette.py +0 -0
  29. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/svg.py +0 -0
  30. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/xtend.py +0 -0
  31. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/fasthtml/xtend.pyi +0 -0
  32. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/pyproject.toml +0 -0
  33. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/python_fasthtml.egg-info/SOURCES.txt +0 -0
  34. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  35. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/python_fasthtml.egg-info/entry_points.txt +0 -0
  36. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/python_fasthtml.egg-info/not-zip-safe +0 -0
  37. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/python_fasthtml.egg-info/requires.txt +0 -0
  38. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/python_fasthtml.egg-info/top_level.txt +0 -0
  39. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/setup.cfg +0 -0
  40. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/setup.py +0 -0
  41. {python-fasthtml-0.9.2 → python-fasthtml-0.10.1}/tests/test_toaster.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.9.2
3
+ Version: 0.10.1
4
4
  Summary: The fastest way to create an HTML app
5
5
  Home-page: https://github.com/AnswerDotAI/fasthtml
6
6
  Author: Jeremy Howard and contributors
@@ -0,0 +1,2 @@
1
+ __version__ = "0.10.1"
2
+ from .core import *
@@ -131,7 +131,13 @@ d = { 'settings': { 'branch': 'main',
131
131
  'fasthtml.jupyter.wait_port_free': ('api/jupyter.html#wait_port_free', 'fasthtml/jupyter.py'),
132
132
  'fasthtml.jupyter.ws_client': ('api/jupyter.html#ws_client', 'fasthtml/jupyter.py')},
133
133
  'fasthtml.live_reload': {},
134
- 'fasthtml.oauth': { 'fasthtml.oauth.DiscordAppClient': ('api/oauth.html#discordappclient', 'fasthtml/oauth.py'),
134
+ 'fasthtml.oauth': { 'fasthtml.oauth.Auth0AppClient': ('api/oauth.html#auth0appclient', 'fasthtml/oauth.py'),
135
+ 'fasthtml.oauth.Auth0AppClient.__init__': ('api/oauth.html#auth0appclient.__init__', 'fasthtml/oauth.py'),
136
+ 'fasthtml.oauth.Auth0AppClient._fetch_openid_config': ( 'api/oauth.html#auth0appclient._fetch_openid_config',
137
+ 'fasthtml/oauth.py'),
138
+ 'fasthtml.oauth.Auth0AppClient.login_link': ( 'api/oauth.html#auth0appclient.login_link',
139
+ 'fasthtml/oauth.py'),
140
+ 'fasthtml.oauth.DiscordAppClient': ('api/oauth.html#discordappclient', 'fasthtml/oauth.py'),
135
141
  'fasthtml.oauth.DiscordAppClient.__init__': ( 'api/oauth.html#discordappclient.__init__',
136
142
  'fasthtml/oauth.py'),
137
143
  'fasthtml.oauth.DiscordAppClient.login_link': ( 'api/oauth.html#discordappclient.login_link',
@@ -121,7 +121,10 @@ def _fill_item(item, obj):
121
121
  if val is not None and not 'skip' in attr:
122
122
  if tag=='input':
123
123
  if attr.get('type', '') == 'checkbox':
124
- if val: attr['checked'] = '1'
124
+ if isinstance(val, list):
125
+ if attr['value'] in val: attr['checked'] = '1'
126
+ else: attr.pop('checked', '')
127
+ elif val: attr['checked'] = '1'
125
128
  else: attr.pop('checked', '')
126
129
  elif attr.get('type', '') == 'radio':
127
130
  if val and val == attr['value']: attr['checked'] = '1'
@@ -129,8 +132,13 @@ def _fill_item(item, obj):
129
132
  else: attr['value'] = val
130
133
  if tag=='textarea': cs=(val,)
131
134
  if tag == 'select':
132
- option = next((o for o in cs if o.tag=='option' and o.get('value')==val), None)
133
- if option: option.selected = '1'
135
+ if isinstance(val, list):
136
+ for opt in cs:
137
+ if opt.tag == 'option' and opt.get('value') in val:
138
+ opt.selected = '1'
139
+ else:
140
+ option = next((o for o in cs if o.tag=='option' and o.get('value')==val), None)
141
+ if option: option.selected = '1'
134
142
  return FT(tag,cs,attr,void_=item.void_)
135
143
 
136
144
  # %% ../nbs/api/01_components.ipynb
@@ -181,17 +189,23 @@ def html2ft(html, attr1st=False):
181
189
  cts = elm.contents
182
190
  cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)
183
191
  for c in cts if str(c).strip()]
184
- attrs = []
192
+ attrs, exotic_attrs = [], {}
185
193
  for key, value in sorted(elm.attrs.items(), key=lambda x: x[0]=='class'):
186
194
  if isinstance(value,(tuple,list)): value = " ".join(value)
187
- key = rev_map.get(key, key)
188
- attrs.append(f'{key.replace("-", "_")}={value!r}'
189
- if _re_h2x_attr_key.match(key) else f'**{{{key!r}:{value!r}}}')
195
+ key, value = rev_map.get(key, key), value or True
196
+ if _re_h2x_attr_key.match(key): attrs.append(f'{key.replace("-", "_")}={value!r}')
197
+ else: exotic_attrs[key] = value
198
+ if exotic_attrs: attrs.append(f'**{exotic_attrs!r}')
190
199
  spc = " "*lvl*indent
191
200
  onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))
192
201
  j = ', ' if onlychild else f',\n{spc}'
193
202
  inner = j.join(filter(None, cs+attrs))
194
- if onlychild: return f'{tag_name}({inner})'
203
+ if onlychild:
204
+ if not attr1st: return f'{tag_name}({inner})'
205
+ else:
206
+ # respect attr1st setting
207
+ attrs = ', '.join(filter(None, attrs))
208
+ return f'{tag_name}({attrs})({cs[0] if cs else ""})'
195
209
  if not attr1st or not attrs: return f'{tag_name}(\n{spc}{inner}\n{" "*(lvl-1)*indent})'
196
210
  inner_cs = j.join(filter(None, cs))
197
211
  inner_attrs = ', '.join(filter(None, attrs))
@@ -4,24 +4,25 @@ from fasthtml.basics import FastHTML, Script
4
4
  __all__ = ["FastHTMLWithLiveReload"]
5
5
 
6
6
 
7
- LIVE_RELOAD_SCRIPT = """
8
- (function() {{
9
- var socket = new WebSocket(`ws://${{window.location.host}}/live-reload`);
10
- var maxReloadAttempts = {reload_attempts};
11
- var reloadInterval = {reload_interval}; // time between reload attempts in ms
12
- socket.onclose = function() {{
7
+ def LiveReloadJs(reload_attempts:int=1, reload_interval:float=1000., **kwargs):
8
+ src = """
9
+ (function() {
10
+ var socket = new WebSocket(`ws://${window.location.host}/live-reload`);
11
+ var maxReloadAttempts = %s;
12
+ var reloadInterval = %s; // time between reload attempts in ms
13
+ socket.onclose = function() {
13
14
  let reloadAttempts = 0;
14
- const intervalFn = setInterval(function(){{
15
+ const intervalFn = setInterval(function(){
15
16
  window.location.reload();
16
17
  reloadAttempts++;
17
18
  if (reloadAttempts === maxReloadAttempts) clearInterval(intervalFn);
18
- }}, reloadInterval);
19
- }}
20
- }})();
19
+ }, reloadInterval);
20
+ }
21
+ })();
21
22
  """
23
+ return Script(src % (reload_attempts, reload_interval))
22
24
 
23
-
24
- async def live_reload_websocket(websocket): await websocket.accept()
25
+ async def live_reload_ws(websocket): await websocket.accept()
25
26
 
26
27
  class FastHTMLWithLiveReload(FastHTML):
27
28
  """
@@ -47,19 +48,8 @@ class FastHTMLWithLiveReload(FastHTML):
47
48
  Run:
48
49
  serve()
49
50
  """
50
- LIVE_RELOAD_ROUTE = WebSocketRoute("/live-reload", endpoint=live_reload_websocket)
51
-
52
51
  def __init__(self, *args, **kwargs):
53
- # Create the live reload script to be injected into the webpage
54
- self.LIVE_RELOAD_HEADER = Script(
55
- LIVE_RELOAD_SCRIPT.format(
56
- reload_attempts=kwargs.get("reload_attempts", 1),
57
- reload_interval=kwargs.get("reload_interval", 1000),
58
- )
59
- )
60
-
61
52
  # "hdrs" and "routes" can be missing, None, a list or a tuple.
62
- kwargs["hdrs"] = [*(kwargs.get("hdrs") or []), self.LIVE_RELOAD_HEADER]
63
- kwargs["routes"] = [*(kwargs.get("routes") or []), self.LIVE_RELOAD_ROUTE]
53
+ kwargs["hdrs"] = [*(kwargs.get("hdrs") or []), LiveReloadJs(**kwargs)]
54
+ kwargs["routes"] = [*(kwargs.get("routes") or []), WebSocketRoute("/live-reload", endpoint=live_reload_ws)]
64
55
  super().__init__(*args, **kwargs)
65
-
@@ -3,8 +3,8 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/08_oauth.ipynb.
4
4
 
5
5
  # %% auto 0
6
- __all__ = ['http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'HuggingFaceClient', 'DiscordAppClient', 'redir_url',
7
- 'url_match', 'OAuth']
6
+ __all__ = ['http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'HuggingFaceClient', 'DiscordAppClient', 'Auth0AppClient',
7
+ 'redir_url', 'url_match', 'OAuth']
8
8
 
9
9
  # %% ../nbs/api/08_oauth.ipynb
10
10
  from .common import *
@@ -14,6 +14,7 @@ import secrets, httpx
14
14
 
15
15
  # %% ../nbs/api/08_oauth.ipynb
16
16
  class _AppClient(WebApplicationClient):
17
+ id_key = 'sub'
17
18
  def __init__(self, client_id, client_secret, code=None, scope=None, **kwargs):
18
19
  super().__init__(client_id, code=code, scope=scope, **kwargs)
19
20
  self.client_secret = client_secret
@@ -22,9 +23,8 @@ class _AppClient(WebApplicationClient):
22
23
  class GoogleAppClient(_AppClient):
23
24
  "A `WebApplicationClient` for Google oauth2"
24
25
  base_url = "https://accounts.google.com/o/oauth2/v2/auth"
25
- token_url = "https://www.googleapis.com/oauth2/v4/token"
26
- info_url = "https://www.googleapis.com/oauth2/v3/userinfo"
27
- id_key = 'sub'
26
+ token_url = "https://oauth2.googleapis.com/token"
27
+ info_url = "https://openidconnect.googleapis.com/v1/userinfo"
28
28
 
29
29
  def __init__(self, client_id, client_secret, code=None, scope=None, **kwargs):
30
30
  scope_pre = "https://www.googleapis.com/auth/userinfo"
@@ -39,8 +39,9 @@ class GoogleAppClient(_AppClient):
39
39
  # %% ../nbs/api/08_oauth.ipynb
40
40
  class GitHubAppClient(_AppClient):
41
41
  "A `WebApplicationClient` for GitHub oauth2"
42
- base_url = "https://github.com/login/oauth/authorize"
43
- token_url = "https://github.com/login/oauth/access_token"
42
+ prefix = "https://github.com/login/oauth/"
43
+ base_url = f"{prefix}authorize"
44
+ token_url = f"{prefix}access_token"
44
45
  info_url = "https://api.github.com/user"
45
46
  id_key = 'id'
46
47
 
@@ -50,11 +51,10 @@ class GitHubAppClient(_AppClient):
50
51
  # %% ../nbs/api/08_oauth.ipynb
51
52
  class HuggingFaceClient(_AppClient):
52
53
  "A `WebApplicationClient` for HuggingFace oauth2"
53
-
54
- base_url = "https://huggingface.co/oauth/authorize"
55
- token_url = "https://huggingface.co/oauth/token"
56
- info_url = "https://huggingface.co/oauth/userinfo"
57
- id_key = 'sub'
54
+ prefix = "https://huggingface.co/oauth/"
55
+ base_url = f"{prefix}authorize"
56
+ token_url = f"{prefix}token"
57
+ info_url = f"{prefix}userinfo"
58
58
 
59
59
  def __init__(self, client_id, client_secret, code=None, scope=None, state=None, **kwargs):
60
60
  if not scope: scope=["openid","profile"]
@@ -87,6 +87,24 @@ class DiscordAppClient(_AppClient):
87
87
  r.raise_for_status()
88
88
  self.parse_request_body_response(r.text)
89
89
 
90
+ # %% ../nbs/api/08_oauth.ipynb
91
+ class Auth0AppClient(_AppClient):
92
+ "A `WebApplicationClient` for Auth0 OAuth2"
93
+ def __init__(self, domain, client_id, client_secret, code=None, scope=None, redirect_uri="", **kwargs):
94
+ self.redirect_uri,self.domain = redirect_uri,domain
95
+ config = self._fetch_openid_config()
96
+ self.base_url,self.token_url,self.info_url = config["authorization_endpoint"],config["token_endpoint"],config["userinfo_endpoint"]
97
+ super().__init__(client_id, client_secret, code=code, scope=scope, redirect_uri=redirect_uri, **kwargs)
98
+
99
+ def _fetch_openid_config(self):
100
+ r = httpx.get(f"https://{self.domain}/.well-known/openid-configuration")
101
+ r.raise_for_status()
102
+ return r.json()
103
+
104
+ def login_link(self, req):
105
+ d = dict(response_type="code", client_id=self.client_id, scope=self.scope, redirect_uri=redir_url(req, self.redirect_uri))
106
+ return f"{self.base_url}?{urlencode(d)}"
107
+
90
108
  # %% ../nbs/api/08_oauth.ipynb
91
109
  @patch
92
110
  def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None):
@@ -96,8 +114,9 @@ def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None):
96
114
  return self.prepare_request_uri(self.base_url, redirect_uri, scope, state=state)
97
115
 
98
116
  # %% ../nbs/api/08_oauth.ipynb
99
- def redir_url(request, redir_path, scheme='https'):
117
+ def redir_url(request, redir_path, scheme=None):
100
118
  "Get the redir url for the host in `request`"
119
+ scheme = 'http' if request.url.hostname in ("localhost", "127.0.0.1") else 'https'
101
120
  return f"{scheme}://{request.url.netloc}{redir_path}"
102
121
 
103
122
  # %% ../nbs/api/08_oauth.ipynb
@@ -13,9 +13,17 @@ toast_css = """
13
13
  }
14
14
  .fh-toast {
15
15
  background-color: #333; color: white;
16
- padding: 12px 20px; border-radius: 4px; margin-bottom: 10px;
16
+ padding: 12px 28px 12px 20px; border-radius: 4px; margin-bottom: 10px;
17
17
  max-width: 80%; width: auto; text-align: center;
18
18
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
19
+ position: relative;
20
+ }
21
+ .fh-toast-dismiss {
22
+ position:absolute; top:0.2em; right:0.4em;
23
+ line-height:1em; padding: 0 0.2em 0.2em 0.2em; border-radius:inherit;
24
+ transform:scaleY(0.8); transform:scaleX(1.);
25
+ pointer-events:auto; cursor:pointer;
26
+ background-color:inherit; color:inherit; filter:brightness(0.85);
19
27
  }
20
28
  .fh-toast-info { background-color: #2196F3; }
21
29
  .fh-toast-success { background-color: #4CAF50; }
@@ -23,7 +31,9 @@ toast_css = """
23
31
  .fh-toast-error { background-color: #F44336; }
24
32
  """
25
33
 
26
- toast_js = """
34
+ def ToastJs(duration:float):
35
+ duration = int(1000*duration)
36
+ src = """
27
37
  export function proc_htmx(sel, func) {
28
38
  htmx.onLoad(elt => {
29
39
  const elements = any(sel, elt, false);
@@ -34,26 +44,30 @@ export function proc_htmx(sel, func) {
34
44
  proc_htmx('.fh-toast-container', async function(toast) {
35
45
  await sleep(100);
36
46
  toast.style.opacity = '0.8';
37
- await sleep(3000);
47
+ await sleep(%s);
38
48
  toast.style.opacity = '0';
39
49
  await sleep(300);
40
50
  toast.remove();
41
51
  });
52
+
53
+ proc_htmx('.fh-toast-dismiss', function(elem) {
54
+ elem.addEventListener('click', (e) => { e.target.parentElement.remove() });
55
+ });
42
56
  """
57
+ return Script(src % (duration,), type="module")
58
+
43
59
 
44
60
  def add_toast(sess, message, typ="info"):
45
61
  assert typ in ("info", "success", "warning", "error"), '`typ` not in ("info", "success", "warning", "error")'
46
62
  sess.setdefault(sk, []).append((message, typ))
47
63
 
48
64
  def render_toasts(sess):
49
- toasts = [Div(msg, cls=f"fh-toast fh-toast-{typ}") for msg,typ in sess.pop(sk, [])]
50
- return Div(Div(*toasts, cls="fh-toast-container"),
51
- hx_swap_oob="afterbegin:body")
65
+ toasts = [Div(msg, Span('x', cls='fh-toast-dismiss'), cls=f"fh-toast fh-toast-{typ}") for msg,typ in sess.pop(sk, [])]
66
+ return Div(Div(*toasts, cls="fh-toast-container"), hx_swap_oob="afterbegin:body")
52
67
 
53
68
  def toast_after(resp, req, sess):
54
69
  if sk in sess and (not resp or isinstance(resp, (tuple,FT,FtResponse))): req.injects.append(render_toasts(sess))
55
70
 
56
- def setup_toasts(app):
57
- app.hdrs += (Style(toast_css), Script(toast_js, type="module"))
71
+ def setup_toasts(app, duration:float=10.):
72
+ app.hdrs += (Style(toast_css), ToastJs(duration))
58
73
  app.after.append(toast_after)
59
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.9.2
3
+ Version: 0.10.1
4
4
  Summary: The fastest way to create an HTML app
5
5
  Home-page: https://github.com/AnswerDotAI/fasthtml
6
6
  Author: Jeremy Howard and contributors
@@ -1,7 +1,7 @@
1
1
  [DEFAULT]
2
2
  repo = fasthtml
3
3
  lib_name = fasthtml
4
- version = 0.9.2
4
+ version = 0.10.1
5
5
  min_python = 3.10
6
6
  license = apache2
7
7
  requirements = fastcore>=1.7.18 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.0.9 python-multipart beautifulsoup4
@@ -1,2 +0,0 @@
1
- __version__ = "0.9.2"
2
- from .core import *