python-fasthtml 0.0.4__tar.gz → 0.0.6__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 (24) hide show
  1. {python-fasthtml-0.0.4/python_fasthtml.egg-info → python-fasthtml-0.0.6}/PKG-INFO +1 -1
  2. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/__init__.py +2 -2
  3. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/_modidx.py +1 -0
  4. python-fasthtml-0.0.6/fasthtml/authmw.py +45 -0
  5. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/core.py +34 -17
  6. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/fastapp.py +1 -2
  7. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/starlette.py +3 -1
  8. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/xtend.py +12 -12
  9. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6/python_fasthtml.egg-info}/PKG-INFO +1 -1
  10. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/python_fasthtml.egg-info/SOURCES.txt +1 -0
  11. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/settings.ini +2 -1
  12. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/LICENSE +0 -0
  13. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/MANIFEST.in +0 -0
  14. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/README.md +0 -0
  15. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/all.py +0 -0
  16. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/components.py +0 -0
  17. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/fasthtml/oauth.py +0 -0
  18. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  19. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/python_fasthtml.egg-info/entry_points.txt +0 -0
  20. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/python_fasthtml.egg-info/not-zip-safe +0 -0
  21. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/python_fasthtml.egg-info/requires.txt +0 -0
  22. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/python_fasthtml.egg-info/top_level.txt +0 -0
  23. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/setup.cfg +0 -0
  24. {python-fasthtml-0.0.4 → python-fasthtml-0.0.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.0.4
3
+ Version: 0.0.6
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
@@ -1,5 +1,5 @@
1
- __version__ = "0.0.4"
1
+ __version__ = "0.0.6"
2
2
  from .core import *
3
+ from .authmw import *
3
4
  from .components import *
4
5
  from .xtend import *
5
-
@@ -6,6 +6,7 @@ d = { 'settings': { 'branch': 'main',
6
6
  'git_url': 'https://github.com/AnswerDotAI/fasthtml',
7
7
  'lib_path': 'fasthtml'},
8
8
  'syms': { 'fasthtml.all': {},
9
+ 'fasthtml.authmw': {},
9
10
  'fasthtml.components': { 'fasthtml.components._FindElems': ('components.html#_findelems', 'fasthtml/components.py'),
10
11
  'fasthtml.components._FindElems.__init__': ( 'components.html#_findelems.__init__',
11
12
  'fasthtml/components.py'),
@@ -0,0 +1,45 @@
1
+ import base64, binascii
2
+ from fasthtml.core import *
3
+ from fasthtml.starlette import *
4
+ from typing import Mapping
5
+
6
+ auth_hdrs = {'WWW-Authenticate': 'Basic realm="login"'}
7
+
8
+ class BasicAuthMiddleware(MiddlewareBase):
9
+ def __init__(self, app, cb, skip=None): self.app,self.cb,self.skip = app,cb,skip or []
10
+
11
+ async def _resp(self, scope, receive, send, resp):
12
+ await (send({"type": "websocket.close", "code": 1000}) if scope["type"]=="websocket" else resp(scope, receive, send))
13
+
14
+ async def __call__(self, scope, receive, send) -> None:
15
+ conn = await super().__call__(scope, receive, send)
16
+ if not conn: return
17
+ request = Request(scope, receive)
18
+ if not any(re.match(o+'$', request.url.path) for o in self.skip):
19
+ res = await self.authenticate(conn)
20
+ if not res: res = Response('not authenticated', status_code=401, headers=auth_hdrs)
21
+ if isinstance(res, Response): return await self._resp(scope, receive, send, res)
22
+ scope["auth"] = res
23
+ await self.app(scope, receive, send)
24
+
25
+ async def authenticate(self, conn):
26
+ if "Authorization" not in conn.headers: return
27
+ auth = conn.headers["Authorization"]
28
+ try:
29
+ scheme, credentials = auth.split()
30
+ if scheme.lower() != 'basic': return
31
+ decoded = base64.b64decode(credentials).decode("ascii")
32
+ except (ValueError, UnicodeDecodeError, binascii.Error) as exc: raise AuthenticationError('Invalid credentials')
33
+ user, _, pwd = decoded.partition(":")
34
+ if self.cb(user,pwd): return user
35
+
36
+ def user_pwd_auth(lookup=None, skip=None, **kwargs):
37
+ if isinstance(lookup,Mapping): kwargs = lookup | kwargs
38
+ def cb(u,p):
39
+ if u=='logout' or not u or not p: return
40
+ if callable(lookup): return lookup(u,p)
41
+ return kwargs.get(u,None) == p
42
+ return Middleware(BasicAuthMiddleware, cb=cb, skip=skip)
43
+
44
+ def basic_logout(request):
45
+ return f'{request.url.scheme}://logout:logout@{request.headers["host"]}/'
@@ -1,4 +1,4 @@
1
- import json, dateutil, uuid
1
+ import json,dateutil,uuid,inspect
2
2
 
3
3
  from fastcore.utils import *
4
4
  from fastcore.xml import *
@@ -95,6 +95,7 @@ async def _from_body(req, arg, p):
95
95
 
96
96
  async def _find_p(req, arg:str, p):
97
97
  anno = p.annotation
98
+ if arg.lower()=='auth': return req.scope['auth']
98
99
  if isinstance(anno, type):
99
100
  if issubclass(anno, Request): return req
100
101
  if issubclass(anno, HtmxHeaders): return _get_htmx(req)
@@ -110,11 +111,11 @@ async def _find_p(req, arg:str, p):
110
111
  if not res: res = req.query_params.get(arg, None)
111
112
  if not res: res = req.cookies.get(arg, None)
112
113
  if not res: res = req.headers.get(snake2hyphens(arg), None)
113
- if not res: res = nested_idx(req.scope, 'session', arg)
114
- if not res: res = p.default
114
+ if not res: res = nested_idx(req.scope, 'session', arg) or None
115
115
  if res is empty or res is None:
116
116
  body = await req.form()
117
117
  res = body.get(arg, None)
118
+ if not res: res = p.default
118
119
  if not isinstance(res, str) or anno is empty: return res
119
120
  return _fix_anno(anno)(res)
120
121
 
@@ -136,42 +137,49 @@ def _wrap_resp(req, resp, cls, hdrs, **bodykw):
136
137
  if isinstance(resp, Response): return resp
137
138
  if cls is not empty: return cls(resp)
138
139
  if isinstance(resp, (list,tuple)): return _xt_resp(req, resp, hdrs, **bodykw)
139
- if isinstance(resp, str): cls = HTMLResponse
140
- elif isinstance(resp, Mapping): cls = JSONResponse
140
+ if isinstance(resp, str): cls = HTMLResponse
141
+ elif isinstance(resp, Mapping): cls = JSONResponse
141
142
  else:
142
143
  resp = str(resp)
143
144
  cls = HTMLResponse
144
145
  return cls(resp)
145
146
 
146
- def _wrap_ep(f, hdrs, **bodykw):
147
+ def _wrap_ep(f, hdrs, before, **bodykw):
147
148
  if not (isfunction(f) or ismethod(f)): return f
148
149
  sig = signature(f)
149
150
  params = sig.parameters
150
151
  cls = sig.return_annotation
151
152
 
152
153
  async def _f(req):
153
- wreq = await _wrap_req(req, params)
154
- resp = f(*wreq)
155
- if is_async_callable(f): resp = await resp
154
+ resp = None
155
+ for b in before:
156
+ if not resp:
157
+ wreq = await _wrap_req(req, signature(b).parameters)
158
+ resp = b(*wreq)
159
+ if is_async_callable(b): resp = await resp
160
+ if not resp:
161
+ wreq = await _wrap_req(req, params)
162
+ resp = f(*wreq)
163
+ if is_async_callable(f): resp = await resp
156
164
  return _wrap_resp(req, resp, cls, hdrs, **bodykw)
157
165
  return _f
158
166
 
159
167
  class RouteX(Route):
160
168
  def __init__(self, path:str, endpoint, *, methods=None, name=None, include_in_schema=True, middleware=None,
161
- hdrs=None, **bodykw):
162
- super().__init__(path, _wrap_ep(endpoint, hdrs, **bodykw), methods=methods, name=name,
169
+ hdrs=None, before=None, **bodykw):
170
+ super().__init__(path, _wrap_ep(endpoint, hdrs, before, **bodykw), methods=methods, name=name,
163
171
  include_in_schema=include_in_schema, middleware=middleware)
164
172
 
165
173
  class RouterX(Router):
166
174
  def __init__(self, routes=None, redirect_slashes=True, default=None, on_startup=None, on_shutdown=None,
167
- lifespan=None, *, middleware=None, hdrs=None, **bodykw):
175
+ lifespan=None, *, middleware=None, hdrs=None, before=None, **bodykw):
168
176
  super().__init__(routes, redirect_slashes, default, on_startup, on_shutdown,
169
177
  lifespan=lifespan, middleware=middleware)
170
- self.hdrs,self.bodykw = hdrs or (),bodykw
178
+ self.hdrs,self.bodykw,self.before = hdrs or (),bodykw,before or ()
171
179
 
172
180
  def add_route( self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True):
173
181
  route = RouteX(path, endpoint=endpoint, methods=methods, name=name, include_in_schema=include_in_schema,
174
- hdrs=self.hdrs, **self.bodykw)
182
+ hdrs=self.hdrs, before=self.before, **self.bodykw)
175
183
  self.routes = [o for o in self.routes if o.methods!=methods or o.path!=path]
176
184
  self.routes.append(route)
177
185
 
@@ -187,12 +195,14 @@ def get_key(key=None, fname='.sesskey'):
187
195
  fname.write_text(key)
188
196
  return key
189
197
 
198
+ def _list(o): return [] if not o else o if isinstance(o, (tuple,list)) else [o]
199
+
190
200
  class FastHTML(Starlette):
191
201
  def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
192
- on_startup=None, on_shutdown=None, lifespan=None, hdrs=None,
202
+ on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, before=None,
193
203
  secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
194
204
  same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', **bodykw):
195
- if not middleware: middleware = []
205
+ middleware,before = _list(middleware),_list(before)
196
206
  secret_key = get_key(secret_key, key_fname)
197
207
  sess = Middleware(SessionMiddleware, secret_key=secret_key, session_cookie=session_cookie,
198
208
  max_age=max_age, path=sess_path, same_site=same_site,
@@ -200,7 +210,8 @@ class FastHTML(Starlette):
200
210
  middleware.append(sess)
201
211
  super().__init__(debug, routes, middleware, exception_handlers, on_startup, on_shutdown, lifespan=lifespan)
202
212
  hdrs = list([] if hdrs is None else hdrs) + [htmxscr]
203
- self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, hdrs=hdrs, **bodykw)
213
+ self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, hdrs=hdrs,
214
+ before=before, **bodykw)
204
215
 
205
216
  def route(self, path:str, methods=None, name=None, include_in_schema=True):
206
217
  def f(func):
@@ -220,3 +231,9 @@ def reg_re_param(m, s):
220
231
  reg_re_param("path", ".*?")
221
232
  reg_re_param("static", "ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|xml")
222
233
 
234
+ class MiddlewareBase:
235
+ async def __call__(self, scope, receive, send) -> None:
236
+ if scope["type"] not in ["http", "websocket"]:
237
+ await self.app(scope, receive, send)
238
+ return
239
+ return HTTPConnection(scope)
@@ -16,12 +16,11 @@ def fast_app(db=None, render=None, hdrs=None, tbls=None, **kwargs):
16
16
  async def get(fname:str, ext:str): return FileResponse(f'{fname}.{ext}')
17
17
  if not db: return app
18
18
 
19
+ db = database(db)
19
20
  if not tbls: tbls={}
20
21
  if kwargs:
21
22
  kwargs['render'] = render
22
23
  tbls['items'] = kwargs
23
- db = Database(db)
24
- db.enable_wal()
25
24
  dbtbls = [get_tbl(db.t, k, v) for k,v in tbls.items()]
26
25
  if len(dbtbls)==1: dbtbls=dbtbls[0]
27
26
  return app,*dbtbls
@@ -2,10 +2,11 @@ from starlette.applications import Starlette
2
2
  from starlette.middleware import Middleware
3
3
  from starlette.middleware.sessions import SessionMiddleware
4
4
  from starlette.middleware.authentication import AuthenticationMiddleware
5
+ from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser, requires
5
6
  from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
6
7
  from starlette.middleware.trustedhost import TrustedHostMiddleware
7
8
  from starlette.responses import Response, HTMLResponse, FileResponse, JSONResponse
8
- from starlette.requests import Request
9
+ from starlette.requests import Request, HTTPConnection
9
10
  from starlette.staticfiles import StaticFiles
10
11
  from starlette.exceptions import HTTPException
11
12
  from starlette._utils import is_async_callable
@@ -15,4 +16,5 @@ from starlette.exceptions import HTTPException,WebSocketException
15
16
  from starlette.endpoints import HTTPEndpoint,WebSocketEndpoint
16
17
  from starlette.config import Config
17
18
  from starlette.datastructures import CommaSeparatedStrings, Secret
19
+ from starlette.types import ASGIApp, Receive, Scope, Send
18
20
 
@@ -16,13 +16,13 @@ from .components import *
16
16
  try: from IPython import display
17
17
  except ImportError: display=None
18
18
 
19
- # %% ../nbs/02_xtend.ipynb 3
19
+ # %% ../nbs/02_xtend.ipynb 4
20
20
  picocss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css"
21
21
  picolink = Link(rel="stylesheet", href=picocss)
22
22
  picocondcss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.conditional.min.css"
23
23
  picocondlink = Link(rel="stylesheet", href=picocondcss)
24
24
 
25
- # %% ../nbs/02_xtend.ipynb 6
25
+ # %% ../nbs/02_xtend.ipynb 7
26
26
  def set_pico_cls():
27
27
  js = """var sel = '.cell-output, .output_area';
28
28
  document.querySelectorAll(sel).forEach(e => e.classList.add('pico'));
@@ -40,23 +40,23 @@ new MutationObserver(ms => {
40
40
  }).observe(document.body, { childList: true, subtree: true });"""
41
41
  return display.Javascript(js)
42
42
 
43
- # %% ../nbs/02_xtend.ipynb 9
43
+ # %% ../nbs/02_xtend.ipynb 10
44
44
  def Html(*c, doctype=True, **kwargs):
45
45
  res = xt('html', *c, **kwargs)
46
46
  if not doctype: return res
47
47
  return (xt('!DOCTYPE', html=True), res)
48
48
 
49
- # %% ../nbs/02_xtend.ipynb 10
49
+ # %% ../nbs/02_xtend.ipynb 11
50
50
  @delegates(xt_hx, keep=True)
51
51
  def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', **kwargs):
52
52
  return xt_hx('a', *c, href=href, hx_get=hx_get, target_id=target_id, hx_swap=hx_swap, **kwargs)
53
53
 
54
- # %% ../nbs/02_xtend.ipynb 12
54
+ # %% ../nbs/02_xtend.ipynb 13
55
55
  @delegates(xt_hx, keep=True)
56
56
  def AX(txt, hx_get=None, target_id=None, hx_swap=None, href='#', **kwargs):
57
57
  return xt_hx('a', txt, href=href, hx_get=hx_get, target_id=target_id, hx_swap=hx_swap, **kwargs)
58
58
 
59
- # %% ../nbs/02_xtend.ipynb 14
59
+ # %% ../nbs/02_xtend.ipynb 15
60
60
  @delegates(xt_hx, keep=True)
61
61
  def Checkbox(checked:bool=False, label=None, value="1", **kwargs):
62
62
  if not checked: checked=None
@@ -64,36 +64,36 @@ def Checkbox(checked:bool=False, label=None, value="1", **kwargs):
64
64
  if label: res = Label(res, label)
65
65
  return res
66
66
 
67
- # %% ../nbs/02_xtend.ipynb 16
67
+ # %% ../nbs/02_xtend.ipynb 17
68
68
  @delegates(xt_hx, keep=True)
69
69
  def Card(*c, header=None, footer=None, **kwargs):
70
70
  if header: c = (Header(header),) + c
71
71
  if footer: c += (Footer(footer),)
72
72
  return Article(*c, **kwargs)
73
73
 
74
- # %% ../nbs/02_xtend.ipynb 18
74
+ # %% ../nbs/02_xtend.ipynb 19
75
75
  @delegates(xt_hx, keep=True)
76
76
  def Group(*c, **kwargs):
77
77
  return Fieldset(*c, role="group", **kwargs)
78
78
 
79
- # %% ../nbs/02_xtend.ipynb 20
79
+ # %% ../nbs/02_xtend.ipynb 21
80
80
  @delegates(xt_hx, keep=True)
81
81
  def Search(*c, **kwargs):
82
82
  return Form(*c, role="search", **kwargs)
83
83
 
84
- # %% ../nbs/02_xtend.ipynb 22
84
+ # %% ../nbs/02_xtend.ipynb 23
85
85
  @delegates(xt_hx, keep=True)
86
86
  def Grid(*c, cls='grid', **kwargs):
87
87
  c = tuple(o if isinstance(o,list) else Div(o) for o in c)
88
88
  return xt_hx('div', *c, cls=cls, **kwargs)
89
89
 
90
- # %% ../nbs/02_xtend.ipynb 24
90
+ # %% ../nbs/02_xtend.ipynb 25
91
91
  @delegates(xt_hx, keep=True)
92
92
  def DialogX(*c, open=None, header=None, footer=None, id=None, **kwargs):
93
93
  card = Card(*c, header=header, footer=footer, **kwargs)
94
94
  return Dialog(card, open=open, id=id)
95
95
 
96
- # %% ../nbs/02_xtend.ipynb 26
96
+ # %% ../nbs/02_xtend.ipynb 27
97
97
  @delegates(xt_hx, keep=True)
98
98
  def Hidden(value:str="", **kwargs):
99
99
  return Input(type="hidden", value=value, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.0.4
3
+ Version: 0.0.6
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
@@ -6,6 +6,7 @@ setup.py
6
6
  fasthtml/__init__.py
7
7
  fasthtml/_modidx.py
8
8
  fasthtml/all.py
9
+ fasthtml/authmw.py
9
10
  fasthtml/components.py
10
11
  fasthtml/core.py
11
12
  fasthtml/fastapp.py
@@ -1,12 +1,13 @@
1
1
  [DEFAULT]
2
2
  repo = fasthtml
3
3
  lib_name = fasthtml
4
- version = 0.0.4
4
+ version = 0.0.6
5
5
  min_python = 3.10
6
6
  license = apache2
7
7
  requirements = fastcore python-dateutil starlette oauthlib itsdangerous uvicorn httpx fastlite
8
8
  dev_requirements = ipython lxml
9
9
  black_formatting = False
10
+ conda_user = fastai
10
11
  doc_path = _docs
11
12
  lib_path = fasthtml
12
13
  nbs_path = nbs
File without changes