python-fasthtml 0.12.40__tar.gz → 0.12.42__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.12.40/python_fasthtml.egg-info → python_fasthtml-0.12.42}/PKG-INFO +1 -1
  2. python_fasthtml-0.12.42/fasthtml/__init__.py +2 -0
  3. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/_modidx.py +7 -0
  4. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/core.py +11 -8
  5. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/oauth.py +51 -21
  6. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/starlette.py +1 -1
  7. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42/python_fasthtml.egg-info}/PKG-INFO +1 -1
  8. python_fasthtml-0.12.40/fasthtml/__init__.py +0 -2
  9. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/CONTRIBUTING.md +0 -0
  10. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/LICENSE +0 -0
  11. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/MANIFEST.in +0 -0
  12. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/README.md +0 -0
  13. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/authmw.py +0 -0
  14. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/basics.py +0 -0
  15. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/cli.py +0 -0
  16. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/common.py +0 -0
  17. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/components.py +0 -0
  18. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/components.pyi +0 -0
  19. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/core.pyi +0 -0
  20. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/fastapp.py +0 -0
  21. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/ft.py +0 -0
  22. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/js.py +0 -0
  23. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/jupyter.py +0 -0
  24. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/katex.js +0 -0
  25. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/live_reload.py +0 -0
  26. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/pico.py +0 -0
  27. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/stripe_otp.py +0 -0
  28. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/svg.py +0 -0
  29. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/toaster.py +0 -0
  30. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/xtend.py +0 -0
  31. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/fasthtml/xtend.pyi +0 -0
  32. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/pyproject.toml +0 -0
  33. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/python_fasthtml.egg-info/SOURCES.txt +0 -0
  34. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  35. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/python_fasthtml.egg-info/entry_points.txt +0 -0
  36. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/python_fasthtml.egg-info/requires.txt +0 -0
  37. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/python_fasthtml.egg-info/top_level.txt +0 -0
  38. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/setup.cfg +0 -0
  39. {python_fasthtml-0.12.40 → python_fasthtml-0.12.42}/tests/test_toaster.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fasthtml
3
- Version: 0.12.40
3
+ Version: 0.12.42
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
@@ -0,0 +1,2 @@
1
+ __version__ = "0.12.42"
2
+ from .core import *
@@ -203,10 +203,17 @@ d = { 'settings': { 'branch': 'main',
203
203
  'fasthtml.oauth._AppClient': ('api/oauth.html#_appclient', 'fasthtml/oauth.py'),
204
204
  'fasthtml.oauth._AppClient.__init__': ('api/oauth.html#_appclient.__init__', 'fasthtml/oauth.py'),
205
205
  'fasthtml.oauth._AppClient.get_info': ('api/oauth.html#_appclient.get_info', 'fasthtml/oauth.py'),
206
+ 'fasthtml.oauth._AppClient.get_info_async': ( 'api/oauth.html#_appclient.get_info_async',
207
+ 'fasthtml/oauth.py'),
206
208
  'fasthtml.oauth._AppClient.parse_response': ( 'api/oauth.html#_appclient.parse_response',
207
209
  'fasthtml/oauth.py'),
210
+ 'fasthtml.oauth._AppClient.parse_response_async': ( 'api/oauth.html#_appclient.parse_response_async',
211
+ 'fasthtml/oauth.py'),
208
212
  'fasthtml.oauth._AppClient.retr_id': ('api/oauth.html#_appclient.retr_id', 'fasthtml/oauth.py'),
209
213
  'fasthtml.oauth._AppClient.retr_info': ('api/oauth.html#_appclient.retr_info', 'fasthtml/oauth.py'),
214
+ 'fasthtml.oauth._AppClient.retr_info_async': ( 'api/oauth.html#_appclient.retr_info_async',
215
+ 'fasthtml/oauth.py'),
216
+ 'fasthtml.oauth._arun': ('api/oauth.html#_arun', 'fasthtml/oauth.py'),
210
217
  'fasthtml.oauth.get_host': ('api/oauth.html#get_host', 'fasthtml/oauth.py'),
211
218
  'fasthtml.oauth.load_creds': ('api/oauth.html#load_creds', 'fasthtml/oauth.py'),
212
219
  'fasthtml.oauth.redir_url': ('api/oauth.html#redir_url', 'fasthtml/oauth.py'),
@@ -12,11 +12,12 @@ __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'htmx_exts', 'htmxsrc',
12
12
  'unqid']
13
13
 
14
14
  # %% ../nbs/api/00_core.ipynb #23503b9e
15
- import json,uuid,inspect,types,signal,asyncio,threading,inspect,random,contextlib,httpx,itsdangerous
15
+ import json,uuid,inspect,types,signal,asyncio,threading,inspect,random,contextlib,httpx,itsdangerous,uvicorn
16
16
 
17
17
  from fastcore.utils import *
18
18
  from fastcore.xml import *
19
- from fastcore.meta import use_kwargs_dict
19
+ from fastcore.meta import use_kwargs_dict,delegates
20
+ from fastcore.style import S
20
21
 
21
22
  from types import UnionType, SimpleNamespace as ns, GenericAlias
22
23
  from typing import Optional, get_type_hints, get_args, get_origin, Union, Mapping, TypedDict, List, Any
@@ -183,6 +184,7 @@ async def _find_p(req, arg:str, p:Parameter):
183
184
  if issubclass(anno, HtmxHeaders): return _get_htmx(req.headers)
184
185
  if issubclass(anno, Starlette): return req.scope['app']
185
186
  if _is_body(anno) and 'session'.startswith(arg.lower()): return req.scope.get('session', {})
187
+ if issubclass(anno, State): return req.scope['app'].state
186
188
  if _is_body(anno): return await _from_body(req, p)
187
189
  # If there's no annotation, check for special names
188
190
  if anno is empty:
@@ -193,6 +195,7 @@ async def _find_p(req, arg:str, p:Parameter):
193
195
  if arg.lower()=='htmx': return _get_htmx(req.headers)
194
196
  if arg.lower()=='app': return req.scope['app']
195
197
  if arg.lower()=='body': return (await req.body()).decode()
198
+ if arg.lower()=='state': return req.scope['app'].state
196
199
  if arg.lower() in ('hdrs','ftrs','bodykw','htmlkw'): return getattr(req, arg.lower())
197
200
  if arg!='resp': warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.")
198
201
  return None
@@ -700,15 +703,15 @@ def set_lifespan(self:FastHTML, value):
700
703
  self.router.lifespan_context = value
701
704
 
702
705
  # %% ../nbs/api/00_core.ipynb #3a348474
706
+ @delegates(uvicorn.run)
703
707
  def serve(
704
708
  appname=None, # Name of the module
705
709
  app='app', # App instance to be served
706
710
  host='0.0.0.0', # If host is 0.0.0.0 will convert to localhost
707
711
  port=None, # If port is None it will default to 5001 or the PORT environment variable
708
712
  reload=True, # Default is to reload the app upon code changes
709
- reload_includes:list[str]|str|None=None, # Additional files to watch for changes
710
- reload_excludes:list[str]|str|None=None # Files to ignore for changes
711
- ):
713
+ **kwargs
714
+ ):
712
715
  "Run the app in an async server, with live reload set as the default."
713
716
  bk = inspect.currentframe().f_back
714
717
  glb = bk.f_globals
@@ -716,11 +719,11 @@ def serve(
716
719
  if not appname:
717
720
  if glb.get('__name__')=='__main__': appname = Path(glb.get('__file__', '')).stem
718
721
  elif code.co_name=='main' and bk.f_back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
719
- import uvicorn
720
722
  if appname:
721
723
  if not port: port=int(os.getenv("PORT", default=5001))
722
- print(f'Link: http://{"localhost" if host=="0.0.0.0" else host}:{port}')
723
- uvicorn.run(f'{appname}:{app}', host=host, port=port, reload=reload, reload_includes=reload_includes, reload_excludes=reload_excludes)
724
+ link = f'http://{"localhost" if host=="0.0.0.0" else host}:{port}'
725
+ print('Link: '+ S.light_red.bold(link))
726
+ uvicorn.run(f'{appname}:{app}', host=host, port=port, reload=reload, **kwargs)
724
727
 
725
728
  # %% ../nbs/api/00_core.ipynb #8121968a
726
729
  class Client:
@@ -3,14 +3,17 @@
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', 'Auth0AppClient',
7
- 'AppleAppClient', 'get_host', 'redir_url', 'url_match', 'OAuth', 'load_creds']
6
+ __all__ = ['log', 'http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'HuggingFaceClient', 'DiscordAppClient',
7
+ 'Auth0AppClient', 'AppleAppClient', 'get_host', 'redir_url', 'url_match', 'OAuth', 'load_creds']
8
8
 
9
9
  # %% ../nbs/api/08_oauth.ipynb #793722f2
10
10
  from .common import *
11
11
  from oauthlib.oauth2 import WebApplicationClient
12
12
  from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote
13
- import secrets, httpx, time
13
+ import secrets, httpx, time, asyncio, logging
14
+
15
+ # %% ../nbs/api/08_oauth.ipynb #44aa4a88
16
+ log = logging.getLogger(__name__)
14
17
 
15
18
  # %% ../nbs/api/08_oauth.ipynb #0a078133
16
19
  class _AppClient(WebApplicationClient):
@@ -90,8 +93,8 @@ class DiscordAppClient(_AppClient):
90
93
  headers = {'Content-Type': 'application/x-www-form-urlencoded'}
91
94
  data = dict(grant_type='authorization_code', code=code)
92
95
  if redirect_uri: data['redirect_uri'] = redirect_uri
93
- r = httpx.post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret))
94
- r.raise_for_status()
96
+ r = httpx.post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret)
97
+ ).raise_for_status()
95
98
  self.parse_request_body_response(r.text)
96
99
 
97
100
  # %% ../nbs/api/08_oauth.ipynb #276520b0
@@ -104,9 +107,7 @@ class Auth0AppClient(_AppClient):
104
107
  super().__init__(client_id, client_secret, code=code, scope=scope, redirect_uri=redirect_uri, **kwargs)
105
108
 
106
109
  def _fetch_openid_config(self):
107
- r = httpx.get(f"https://{self.domain}/.well-known/openid-configuration")
108
- r.raise_for_status()
109
- return r.json()
110
+ return httpx.get(f"https://{self.domain}/.well-known/openid-configuration").raise_for_status().json()
110
111
 
111
112
  def login_link(self, req):
112
113
  d = dict(response_type="code", client_id=self.client_id, scope=self.scope, redirect_uri=redir_url(req, self.redirect_uri))
@@ -166,11 +167,9 @@ def parse_response(self:_AppClient, code, redirect_uri):
166
167
  "Get the token from the oauth2 server response"
167
168
  payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,
168
169
  client_secret=self.client_secret, grant_type='authorization_code')
169
- r = httpx.post(self.token_url, data=payload)
170
- r.raise_for_status()
170
+ r = httpx.post(self.token_url, data=payload).raise_for_status()
171
171
  self.parse_request_body_response(r.text)
172
172
 
173
- # %% ../nbs/api/08_oauth.ipynb #6967dbb0
174
173
  @patch
175
174
  def get_info(self:_AppClient, token=None):
176
175
  "Get the info for authenticated user"
@@ -178,13 +177,39 @@ def get_info(self:_AppClient, token=None):
178
177
  headers = {'Authorization': f'Bearer {token}'}
179
178
  return httpx.get(self.info_url, headers=headers).json()
180
179
 
181
- # %% ../nbs/api/08_oauth.ipynb #03702349
182
180
  @patch
183
181
  def retr_info(self:_AppClient, code, redirect_uri):
184
182
  "Combines `parse_response` and `get_info`"
185
183
  self.parse_response(code, redirect_uri)
186
184
  return self.get_info()
187
185
 
186
+ # %% ../nbs/api/08_oauth.ipynb #0ff353ae
187
+ @patch
188
+ async def parse_response_async(self:_AppClient, code, redirect_uri):
189
+ "Get the token from the oauth2 server response"
190
+ payload = dict(code=code, redirect_uri=redirect_uri, client_id=self.client_id,
191
+ client_secret=self.client_secret, grant_type='authorization_code')
192
+ log.debug(f"OAuth token request: redirect_uri={redirect_uri}, code={code[:20]}...")
193
+ async with httpx.AsyncClient() as c:
194
+ r = (await c.post(self.token_url, data=payload))
195
+ log.debug(f"OAuth response: {r.status_code} - {r.text}")
196
+ r.raise_for_status()
197
+ self.parse_request_body_response(r.text)
198
+
199
+ @patch
200
+ async def get_info_async(self:_AppClient, token=None):
201
+ "Get the info for authenticated user"
202
+ if not token: token = self.token["access_token"]
203
+ headers = {'Authorization': f'Bearer {token}'}
204
+ async with httpx.AsyncClient() as c:
205
+ return (await c.get(self.info_url, headers=headers)).raise_for_status().json()
206
+
207
+ @patch
208
+ async def retr_info_async(self:_AppClient, code, redirect_uri):
209
+ "Combines `parse_response` and `get_info`"
210
+ await self.parse_response_async(code, redirect_uri)
211
+ return await self.get_info_async()
212
+
188
213
  # %% ../nbs/api/08_oauth.ipynb #29f52061
189
214
  @patch
190
215
  def retr_id(self:_AppClient, code, redirect_uri):
@@ -197,38 +222,43 @@ def url_match(request, patterns=http_patterns):
197
222
  return any(re.match(pattern, get_host(request).split(':')[0]) for pattern in patterns)
198
223
 
199
224
  # %% ../nbs/api/08_oauth.ipynb #dda68390
225
+ async def _arun(res): return await res if asyncio.iscoroutine(res) else res
226
+
227
+ # %% ../nbs/api/08_oauth.ipynb #c09a79ff
200
228
  class OAuth:
201
- def __init__(self, app, cli, skip=None, redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login', https=True, http_patterns=http_patterns, redir_method='get'):
229
+ def __init__(self, app, cli, skip=None,
230
+ redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login',
231
+ https=True, http_patterns=http_patterns, redir_method='get'):
202
232
  if not skip: skip = [redir_path,error_path,login_path]
203
233
  redir_handler = app.post if redir_method == 'post' else app.get
204
234
  store_attr()
205
- def before(req, session):
235
+ async def before(req, session):
206
236
  if 'auth' not in req.scope: req.scope['auth'] = session.get('auth')
207
237
  auth = req.scope['auth']
208
238
  if not auth: return self.redir_login(session)
209
- res = self.check_invalid(req, session, auth)
239
+ res = await _arun(self.check_invalid(req, session, auth))
210
240
  if res: return res
211
241
  app.before.append(Beforeware(before, skip=skip))
212
242
 
213
243
  @redir_handler(redir_path)
214
- def redirect(req, session, code:str=None, error:str=None, state:str=None):
244
+ async def redirect(req, session, code:str=None, error:str=None, state:str=None):
215
245
  if not code:
216
246
  session['oauth_error']=error
217
247
  return RedirectResponse(self.error_path, status_code=303)
218
248
  scheme = 'http' if url_match(req,self.http_patterns) or not self.https else 'https'
219
249
  base_url = f"{scheme}://{get_host(req)}"
220
- info = AttrDictDefault(cli.retr_info(code, base_url+redir_path))
250
+ info = AttrDictDefault(await cli.retr_info_async(code, base_url+redir_path))
221
251
  ident = info.get(self.cli.id_key)
222
252
  if not ident: return self.redir_login(session)
223
- res = self.get_auth(info, ident, session, state)
224
- if not res: return self.redir_login(session)
253
+ res = await _arun(self.get_auth(info, ident, session, state))
254
+ if not res: return self.redir_login(session)
225
255
  req.scope['auth'] = session['auth'] = ident
226
256
  return res
227
257
 
228
258
  @app.get(logout_path)
229
- def logout(session):
259
+ async def logout(session):
230
260
  session.pop('auth', None)
231
- return self.logout(session)
261
+ return await _arun(self.logout(session))
232
262
 
233
263
  def redir_login(self, session): return RedirectResponse(self.login_path, status_code=303)
234
264
  def redir_url(self, req):
@@ -16,7 +16,7 @@ from starlette.routing import Route, Router, Mount, WebSocketRoute
16
16
  from starlette.exceptions import HTTPException,WebSocketException
17
17
  from starlette.endpoints import HTTPEndpoint,WebSocketEndpoint
18
18
  from starlette.config import Config
19
- from starlette.datastructures import CommaSeparatedStrings, Secret, UploadFile, URLPath
19
+ from starlette.datastructures import CommaSeparatedStrings, Secret, UploadFile, URLPath, State
20
20
  from starlette.types import ASGIApp, Receive, Scope, Send
21
21
  from starlette.concurrency import run_in_threadpool
22
22
  from starlette.background import BackgroundTask, BackgroundTasks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fasthtml
3
- Version: 0.12.40
3
+ Version: 0.12.42
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
@@ -1,2 +0,0 @@
1
- __version__ = "0.12.40"
2
- from .core import *