python-fasthtml 0.4.2__tar.gz → 0.4.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.4.2/python_fasthtml.egg-info → python-fasthtml-0.4.4}/PKG-INFO +2 -2
- python-fasthtml-0.4.4/fasthtml/__init__.py +2 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/_modidx.py +31 -24
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/common.py +1 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/core.py +39 -42
- python-fasthtml-0.4.4/fasthtml/core.pyi +233 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/fastapp.py +1 -0
- python-fasthtml-0.4.4/fasthtml/oauth.py +130 -0
- python-fasthtml-0.4.4/fasthtml/pico.py +83 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/xtend.py +7 -74
- python-fasthtml-0.4.4/fasthtml/xtend.pyi +107 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4/python_fasthtml.egg-info}/PKG-INFO +2 -2
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/SOURCES.txt +2 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/requires.txt +1 -1
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/settings.ini +2 -2
- python-fasthtml-0.4.2/fasthtml/__init__.py +0 -2
- python-fasthtml-0.4.2/fasthtml/oauth.py +0 -94
- python-fasthtml-0.4.2/fasthtml/xtend.pyi +0 -138
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/CONTRIBUTING.md +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/LICENSE +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/MANIFEST.in +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/README.md +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/authmw.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/basics.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/cli.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/components.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/components.pyi +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/ft.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/js.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/katex.js +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/live_reload.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/starlette.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/svg.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/toaster.py +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/dependency_links.txt +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/entry_points.txt +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/not-zip-safe +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/top_level.txt +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/setup.cfg +0 -0
- {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-fasthtml
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -22,7 +22,7 @@ Requires-Dist: oauthlib
|
|
|
22
22
|
Requires-Dist: itsdangerous
|
|
23
23
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
24
24
|
Requires-Dist: httpx
|
|
25
|
-
Requires-Dist: fastlite>=0.0.
|
|
25
|
+
Requires-Dist: fastlite>=0.0.9
|
|
26
26
|
Requires-Dist: python-multipart
|
|
27
27
|
Requires-Dist: beautifulsoup4
|
|
28
28
|
Provides-Extra: dev
|
|
@@ -45,9 +45,6 @@ d = { 'settings': { 'branch': 'main',
|
|
|
45
45
|
'fasthtml.core.StringConvertor.to_string': ('api/core.html#stringconvertor.to_string', 'fasthtml/core.py'),
|
|
46
46
|
'fasthtml.core.WS_RouteX': ('api/core.html#ws_routex', 'fasthtml/core.py'),
|
|
47
47
|
'fasthtml.core.WS_RouteX.__init__': ('api/core.html#ws_routex.__init__', 'fasthtml/core.py'),
|
|
48
|
-
'fasthtml.core._SessionMiddleware': ('api/core.html#_sessionmiddleware', 'fasthtml/core.py'),
|
|
49
|
-
'fasthtml.core._SessionMiddleware.__call__': ( 'api/core.html#_sessionmiddleware.__call__',
|
|
50
|
-
'fasthtml/core.py'),
|
|
51
48
|
'fasthtml.core._annotations': ('api/core.html#_annotations', 'fasthtml/core.py'),
|
|
52
49
|
'fasthtml.core._apply_ft': ('api/core.html#_apply_ft', 'fasthtml/core.py'),
|
|
53
50
|
'fasthtml.core._find_p': ('api/core.html#_find_p', 'fasthtml/core.py'),
|
|
@@ -77,6 +74,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
77
74
|
'fasthtml.core.cookie': ('api/core.html#cookie', 'fasthtml/core.py'),
|
|
78
75
|
'fasthtml.core.date': ('api/core.html#date', 'fasthtml/core.py'),
|
|
79
76
|
'fasthtml.core.decode_uri': ('api/core.html#decode_uri', 'fasthtml/core.py'),
|
|
77
|
+
'fasthtml.core.flat_tuple': ('api/core.html#flat_tuple', 'fasthtml/core.py'),
|
|
80
78
|
'fasthtml.core.flat_xt': ('api/core.html#flat_xt', 'fasthtml/core.py'),
|
|
81
79
|
'fasthtml.core.form2dict': ('api/core.html#form2dict', 'fasthtml/core.py'),
|
|
82
80
|
'fasthtml.core.get_key': ('api/core.html#get_key', 'fasthtml/core.py'),
|
|
@@ -98,22 +96,38 @@ d = { 'settings': { 'branch': 'main',
|
|
|
98
96
|
'fasthtml.js.dark_media': ('api/js.html#dark_media', 'fasthtml/js.py'),
|
|
99
97
|
'fasthtml.js.light_media': ('api/js.html#light_media', 'fasthtml/js.py')},
|
|
100
98
|
'fasthtml.live_reload': {},
|
|
101
|
-
'fasthtml.oauth': { 'fasthtml.oauth.
|
|
102
|
-
'fasthtml.oauth.
|
|
103
|
-
|
|
104
|
-
'fasthtml.oauth.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
'fasthtml.oauth': { 'fasthtml.oauth.DiscordAppClient': ('api/oauth.html#discordappclient', 'fasthtml/oauth.py'),
|
|
100
|
+
'fasthtml.oauth.DiscordAppClient.__init__': ( 'api/oauth.html#discordappclient.__init__',
|
|
101
|
+
'fasthtml/oauth.py'),
|
|
102
|
+
'fasthtml.oauth.DiscordAppClient.login_link': ( 'api/oauth.html#discordappclient.login_link',
|
|
103
|
+
'fasthtml/oauth.py'),
|
|
104
|
+
'fasthtml.oauth.DiscordAppClient.parse_response': ( 'api/oauth.html#discordappclient.parse_response',
|
|
105
|
+
'fasthtml/oauth.py'),
|
|
106
|
+
'fasthtml.oauth.GitHubAppClient': ('api/oauth.html#githubappclient', 'fasthtml/oauth.py'),
|
|
107
|
+
'fasthtml.oauth.GitHubAppClient.__init__': ('api/oauth.html#githubappclient.__init__', 'fasthtml/oauth.py'),
|
|
108
|
+
'fasthtml.oauth.GoogleAppClient': ('api/oauth.html#googleappclient', 'fasthtml/oauth.py'),
|
|
109
|
+
'fasthtml.oauth.GoogleAppClient.__init__': ('api/oauth.html#googleappclient.__init__', 'fasthtml/oauth.py'),
|
|
110
|
+
'fasthtml.oauth.HuggingFaceClient': ('api/oauth.html#huggingfaceclient', 'fasthtml/oauth.py'),
|
|
111
|
+
'fasthtml.oauth.HuggingFaceClient.__init__': ( 'api/oauth.html#huggingfaceclient.__init__',
|
|
112
|
+
'fasthtml/oauth.py'),
|
|
113
|
+
'fasthtml.oauth.WebApplicationClient.login_link': ( 'api/oauth.html#webapplicationclient.login_link',
|
|
108
114
|
'fasthtml/oauth.py'),
|
|
109
|
-
'fasthtml.oauth.
|
|
110
|
-
|
|
111
|
-
'fasthtml.oauth._AppClient
|
|
112
|
-
'fasthtml.oauth._AppClient.
|
|
115
|
+
'fasthtml.oauth.WebApplicationClient.login_link_with_state': ( 'api/oauth.html#webapplicationclient.login_link_with_state',
|
|
116
|
+
'fasthtml/oauth.py'),
|
|
117
|
+
'fasthtml.oauth._AppClient': ('api/oauth.html#_appclient', 'fasthtml/oauth.py'),
|
|
118
|
+
'fasthtml.oauth._AppClient.__init__': ('api/oauth.html#_appclient.__init__', 'fasthtml/oauth.py'),
|
|
119
|
+
'fasthtml.oauth._AppClient.get_info': ('api/oauth.html#_appclient.get_info', 'fasthtml/oauth.py'),
|
|
120
|
+
'fasthtml.oauth._AppClient.parse_response': ( 'api/oauth.html#_appclient.parse_response',
|
|
113
121
|
'fasthtml/oauth.py'),
|
|
114
|
-
'fasthtml.oauth._AppClient.retr_id': ('
|
|
115
|
-
'fasthtml.oauth._AppClient.retr_info': ('
|
|
116
|
-
|
|
122
|
+
'fasthtml.oauth._AppClient.retr_id': ('api/oauth.html#_appclient.retr_id', 'fasthtml/oauth.py'),
|
|
123
|
+
'fasthtml.oauth._AppClient.retr_info': ('api/oauth.html#_appclient.retr_info', 'fasthtml/oauth.py')},
|
|
124
|
+
'fasthtml.pico': { 'fasthtml.pico.Card': ('api/pico.html#card', 'fasthtml/pico.py'),
|
|
125
|
+
'fasthtml.pico.Container': ('api/pico.html#container', 'fasthtml/pico.py'),
|
|
126
|
+
'fasthtml.pico.DialogX': ('api/pico.html#dialogx', 'fasthtml/pico.py'),
|
|
127
|
+
'fasthtml.pico.Grid': ('api/pico.html#grid', 'fasthtml/pico.py'),
|
|
128
|
+
'fasthtml.pico.Group': ('api/pico.html#group', 'fasthtml/pico.py'),
|
|
129
|
+
'fasthtml.pico.Search': ('api/pico.html#search', 'fasthtml/pico.py'),
|
|
130
|
+
'fasthtml.pico.set_pico_cls': ('api/pico.html#set_pico_cls', 'fasthtml/pico.py')},
|
|
117
131
|
'fasthtml.starlette': {},
|
|
118
132
|
'fasthtml.svg': {},
|
|
119
133
|
'fasthtml.toaster': {},
|
|
@@ -121,21 +135,15 @@ d = { 'settings': { 'branch': 'main',
|
|
|
121
135
|
'fasthtml.xtend.AX': ('api/xtend.html#ax', 'fasthtml/xtend.py'),
|
|
122
136
|
'fasthtml.xtend.Any': ('api/xtend.html#any', 'fasthtml/xtend.py'),
|
|
123
137
|
'fasthtml.xtend.AnyNow': ('api/xtend.html#anynow', 'fasthtml/xtend.py'),
|
|
124
|
-
'fasthtml.xtend.Card': ('api/xtend.html#card', 'fasthtml/xtend.py'),
|
|
125
138
|
'fasthtml.xtend.CheckboxX': ('api/xtend.html#checkboxx', 'fasthtml/xtend.py'),
|
|
126
|
-
'fasthtml.xtend.Container': ('api/xtend.html#container', 'fasthtml/xtend.py'),
|
|
127
|
-
'fasthtml.xtend.DialogX': ('api/xtend.html#dialogx', 'fasthtml/xtend.py'),
|
|
128
139
|
'fasthtml.xtend.Favicon': ('api/xtend.html#favicon', 'fasthtml/xtend.py'),
|
|
129
140
|
'fasthtml.xtend.Form': ('api/xtend.html#form', 'fasthtml/xtend.py'),
|
|
130
|
-
'fasthtml.xtend.Grid': ('api/xtend.html#grid', 'fasthtml/xtend.py'),
|
|
131
|
-
'fasthtml.xtend.Group': ('api/xtend.html#group', 'fasthtml/xtend.py'),
|
|
132
141
|
'fasthtml.xtend.Hidden': ('api/xtend.html#hidden', 'fasthtml/xtend.py'),
|
|
133
142
|
'fasthtml.xtend.Now': ('api/xtend.html#now', 'fasthtml/xtend.py'),
|
|
134
143
|
'fasthtml.xtend.On': ('api/xtend.html#on', 'fasthtml/xtend.py'),
|
|
135
144
|
'fasthtml.xtend.Prev': ('api/xtend.html#prev', 'fasthtml/xtend.py'),
|
|
136
145
|
'fasthtml.xtend.Script': ('api/xtend.html#script', 'fasthtml/xtend.py'),
|
|
137
146
|
'fasthtml.xtend.ScriptX': ('api/xtend.html#scriptx', 'fasthtml/xtend.py'),
|
|
138
|
-
'fasthtml.xtend.Search': ('api/xtend.html#search', 'fasthtml/xtend.py'),
|
|
139
147
|
'fasthtml.xtend.Socials': ('api/xtend.html#socials', 'fasthtml/xtend.py'),
|
|
140
148
|
'fasthtml.xtend.Style': ('api/xtend.html#style', 'fasthtml/xtend.py'),
|
|
141
149
|
'fasthtml.xtend.StyleX': ('api/xtend.html#stylex', 'fasthtml/xtend.py'),
|
|
@@ -146,5 +154,4 @@ d = { 'settings': { 'branch': 'main',
|
|
|
146
154
|
'fasthtml.xtend.loose_format': ('api/xtend.html#loose_format', 'fasthtml/xtend.py'),
|
|
147
155
|
'fasthtml.xtend.replace_css_vars': ('api/xtend.html#replace_css_vars', 'fasthtml/xtend.py'),
|
|
148
156
|
'fasthtml.xtend.run_js': ('api/xtend.html#run_js', 'fasthtml/xtend.py'),
|
|
149
|
-
'fasthtml.xtend.set_pico_cls': ('api/xtend.html#set_pico_cls', 'fasthtml/xtend.py'),
|
|
150
157
|
'fasthtml.xtend.undouble_braces': ('api/xtend.html#undouble_braces', 'fasthtml/xtend.py')}}}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# %% auto 0
|
|
4
4
|
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmxscr', 'htmxwsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths',
|
|
5
5
|
'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', 'form2dict', 'flat_xt', 'Beforeware',
|
|
6
|
-
'WS_RouteX', 'uri', 'decode_uri', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'serve',
|
|
7
|
-
'reg_re_param', 'MiddlewareBase']
|
|
6
|
+
'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'serve',
|
|
7
|
+
'cookie', 'reg_re_param', 'MiddlewareBase']
|
|
8
8
|
|
|
9
9
|
# %% ../nbs/api/00_core.ipynb
|
|
10
10
|
import json,uuid,inspect,types,uvicorn
|
|
@@ -152,6 +152,7 @@ async def _find_p(req, arg:str, p:Parameter):
|
|
|
152
152
|
if arg.lower()=='auth': return req.scope.get('auth', None)
|
|
153
153
|
if arg.lower()=='htmx': return _get_htmx(req.headers)
|
|
154
154
|
if arg.lower()=='app': return req.scope['app']
|
|
155
|
+
if arg.lower()=='body': return (await req.body()).decode()
|
|
155
156
|
if arg.lower() in ('hdrs','ftrs','bodykw','htmlkw'): return getattr(req, arg.lower())
|
|
156
157
|
warn(f"`{arg} has no type annotation and is not a recognised special name, so is ignored.")
|
|
157
158
|
return None
|
|
@@ -310,9 +311,20 @@ def _to_xml(req, resp, indent):
|
|
|
310
311
|
_find_targets(req, resp)
|
|
311
312
|
return to_xml(resp, indent)
|
|
312
313
|
|
|
314
|
+
# %% ../nbs/api/00_core.ipynb
|
|
315
|
+
def flat_tuple(o):
|
|
316
|
+
"Flatten lists"
|
|
317
|
+
result = []
|
|
318
|
+
if not isinstance(o,(tuple,list)): o=[o]
|
|
319
|
+
o = list(o)
|
|
320
|
+
for item in o:
|
|
321
|
+
if isinstance(item, (list,tuple)): result.extend(item)
|
|
322
|
+
else: result.append(item)
|
|
323
|
+
return tuple(result)
|
|
324
|
+
|
|
313
325
|
# %% ../nbs/api/00_core.ipynb
|
|
314
326
|
def _xt_resp(req, resp):
|
|
315
|
-
|
|
327
|
+
resp = flat_tuple(resp)
|
|
316
328
|
resp = resp + tuple(getattr(req, 'injects', ()))
|
|
317
329
|
http_hdrs,resp = partition(resp, risinstance(HttpHeader))
|
|
318
330
|
http_hdrs = {o.k:str(o.v) for o in http_hdrs}
|
|
@@ -415,27 +427,20 @@ def _wrap_ex(f, hdrs, ftrs, htmlkw, bodykw):
|
|
|
415
427
|
return _f
|
|
416
428
|
|
|
417
429
|
# %% ../nbs/api/00_core.ipynb
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
async def receive_wrapper():
|
|
426
|
-
message = await receive()
|
|
427
|
-
if "session" in scope and not isinstance(scope["session"], AttrDict):
|
|
428
|
-
scope["session"] = AttrDict(scope["session"])
|
|
429
|
-
return message
|
|
430
|
-
|
|
431
|
-
await super().__call__(scope, receive_wrapper, send)
|
|
430
|
+
def _mk_locfunc(f,p):
|
|
431
|
+
class _lf:
|
|
432
|
+
def __init__(self): update_wrapper(self, f)
|
|
433
|
+
def __call__(self, *args, **kw): return f(*args, **kw)
|
|
434
|
+
def rt(self, **kw): return p + (f'?{urlencode(kw)}' if kw else '')
|
|
435
|
+
def __str__(self): return p
|
|
436
|
+
return _lf()
|
|
432
437
|
|
|
433
438
|
# %% ../nbs/api/00_core.ipynb
|
|
434
439
|
class FastHTML(Starlette):
|
|
435
440
|
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
|
|
436
441
|
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None,
|
|
437
442
|
before=None, after=None, ws_hdr=False,
|
|
438
|
-
surreal=True, htmx=True, default_hdrs=True, sess_cls=
|
|
443
|
+
surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
|
|
439
444
|
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
|
|
440
445
|
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
|
|
441
446
|
htmlkw=None, **bodykw):
|
|
@@ -459,37 +464,29 @@ class FastHTML(Starlette):
|
|
|
459
464
|
hdrs=hdrs, ftrs=ftrs, before=before, after=after, htmlkw=htmlkw, **bodykw)
|
|
460
465
|
|
|
461
466
|
def ws(self, path:str, conn=None, disconn=None, name=None):
|
|
467
|
+
"Add a websocket route at `path`"
|
|
462
468
|
def f(func):
|
|
463
469
|
self.router.add_ws(path, func, conn=conn, disconn=disconn, name=name)
|
|
464
470
|
return func
|
|
465
471
|
return f
|
|
466
472
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
def route(self, path:str=None, methods=None, name=None, include_in_schema=True):
|
|
474
|
+
"Add a route at `path`"
|
|
475
|
+
pathstr = None if callable(path) else path
|
|
476
|
+
def f(func):
|
|
477
|
+
n,fn,p = name,func.__name__,pathstr
|
|
478
|
+
assert path or (fn not in _verbs), "Must provide a path when using http verb-based function name"
|
|
479
|
+
if methods: m = [methods] if isinstance(methods,str) else methods
|
|
480
|
+
else: m = [fn] if fn in _verbs else ['get','post']
|
|
481
|
+
if not n: n = fn
|
|
482
|
+
if not p: p = '/'+('' if fn=='index' else fn)
|
|
483
|
+
self.router.add_route(p, func, methods=m, name=n, include_in_schema=include_in_schema)
|
|
484
|
+
lf = _mk_locfunc(func, p)
|
|
485
|
+
lf.__routename__ = n
|
|
486
|
+
return lf
|
|
487
|
+
return f(path) if callable(path) else f
|
|
474
488
|
|
|
475
489
|
# %% ../nbs/api/00_core.ipynb
|
|
476
|
-
@patch
|
|
477
|
-
def route(self:FastHTML, path:str=None, methods=None, name=None, include_in_schema=True):
|
|
478
|
-
"Add a route at `path`; the function name is the default method"
|
|
479
|
-
pathstr = None if callable(path) else path
|
|
480
|
-
def f(func):
|
|
481
|
-
n,fn,p = name,func.__name__,pathstr
|
|
482
|
-
assert path or (fn not in _verbs), "Must provide a path when using http verb-based function name"
|
|
483
|
-
if methods: m = [methods] if isinstance(methods,str) else methods
|
|
484
|
-
else: m = [fn] if fn in _verbs else ['get'] if fn=='index' else ['post']
|
|
485
|
-
if not n: n = fn
|
|
486
|
-
if not p: p = '/'+('' if fn=='index' else fn)
|
|
487
|
-
self.router.add_route(p, func, methods=m, name=n, include_in_schema=include_in_schema)
|
|
488
|
-
lf = _mk_locfunc(func, p)
|
|
489
|
-
lf.__routename__ = n
|
|
490
|
-
return lf
|
|
491
|
-
return f(path) if callable(path) else f
|
|
492
|
-
|
|
493
490
|
all_meths = 'get post put delete patch head trace options'.split()
|
|
494
491
|
for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
|
|
495
492
|
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmxscr', 'htmxwsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths', 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', 'form2dict', 'flat_xt', 'Beforeware', 'WS_RouteX', 'uri', 'decode_uri', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'serve', 'cookie', 'reg_re_param', 'MiddlewareBase']
|
|
2
|
+
import json, uuid, inspect, types, uvicorn
|
|
3
|
+
from fastcore.utils import *
|
|
4
|
+
from fastcore.xml import *
|
|
5
|
+
from types import UnionType, SimpleNamespace as ns, GenericAlias
|
|
6
|
+
from typing import Optional, get_type_hints, get_args, get_origin, Union, Mapping, TypedDict, List, Any
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from dataclasses import dataclass, fields
|
|
9
|
+
from collections import namedtuple
|
|
10
|
+
from inspect import isfunction, ismethod, Parameter, get_annotations
|
|
11
|
+
from functools import wraps, partialmethod, update_wrapper
|
|
12
|
+
from http import cookies
|
|
13
|
+
from urllib.parse import urlencode, parse_qs, quote, unquote
|
|
14
|
+
from copy import copy, deepcopy
|
|
15
|
+
from warnings import warn
|
|
16
|
+
from dateutil import parser as dtparse
|
|
17
|
+
from starlette.requests import HTTPConnection
|
|
18
|
+
from .starlette import *
|
|
19
|
+
empty = Parameter.empty
|
|
20
|
+
|
|
21
|
+
def _sig(f):
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def date(s: str):
|
|
25
|
+
"""Convert `s` to a datetime"""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def snake2hyphens(s: str):
|
|
29
|
+
"""Convert `s` from snake case to hyphenated and capitalised"""
|
|
30
|
+
...
|
|
31
|
+
htmx_hdrs = dict(boosted='HX-Boosted', current_url='HX-Current-URL', history_restore_request='HX-History-Restore-Request', prompt='HX-Prompt', request='HX-Request', target='HX-Target', trigger_name='HX-Trigger-Name', trigger='HX-Trigger')
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class HtmxHeaders:
|
|
35
|
+
boosted: str | None = None
|
|
36
|
+
current_url: str | None = None
|
|
37
|
+
history_restore_request: str | None = None
|
|
38
|
+
prompt: str | None = None
|
|
39
|
+
request: str | None = None
|
|
40
|
+
target: str | None = None
|
|
41
|
+
trigger_name: str | None = None
|
|
42
|
+
trigger: str | None = None
|
|
43
|
+
|
|
44
|
+
def __bool__(self):
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def _get_htmx(h):
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def str2int(s) -> int:
|
|
51
|
+
"""Convert `s` to an `int`"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def _mk_list(t, v):
|
|
55
|
+
...
|
|
56
|
+
fh_cfg = AttrDict(indent=True)
|
|
57
|
+
|
|
58
|
+
def _fix_anno(t):
|
|
59
|
+
"""Create appropriate callable type for casting a `str` to type `t` (or first type in `t` if union)"""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def _form_arg(k, v, d):
|
|
63
|
+
"""Get type by accessing key `k` from `d`, and use to cast `v`"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class HttpHeader:
|
|
68
|
+
k: str
|
|
69
|
+
v: str
|
|
70
|
+
|
|
71
|
+
def _annotations(anno):
|
|
72
|
+
"""Same as `get_annotations`, but also works on namedtuples"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def _is_body(anno):
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def _formitem(form, k):
|
|
79
|
+
"""Return single item `k` from `form` if len 1, otherwise return list"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
def form2dict(form: FormData) -> dict:
|
|
83
|
+
"""Convert starlette form data to a dict"""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
async def _from_body(req, p):
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
async def _find_p(req, arg: str, p: Parameter):
|
|
90
|
+
"""In `req` find param named `arg` of type in `p` (`arg` is ignored for body types)"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
async def _wrap_req(req, params):
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
def flat_xt(lst):
|
|
97
|
+
"""Flatten lists"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
class Beforeware:
|
|
101
|
+
|
|
102
|
+
def __init__(self, f, skip=None):
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
async def _handle(f, args, **kwargs):
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
def _find_wsp(ws, data, hdrs, arg: str, p: Parameter):
|
|
109
|
+
"""In `data` find param named `arg` of type in `p` (`arg` is ignored for body types)"""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def _wrap_ws(ws, data, params):
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
async def _send_ws(ws, resp):
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
def _ws_endp(recv, conn=None, disconn=None, hdrs=None, before=None):
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
class WS_RouteX(WebSocketRoute):
|
|
122
|
+
|
|
123
|
+
def __init__(self, path: str, recv, conn: callable=None, disconn: callable=None, *, name=None, middleware=None, hdrs=None, before=None):
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
def uri(_arg, **kwargs):
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
def decode_uri(s):
|
|
130
|
+
...
|
|
131
|
+
from starlette.convertors import StringConvertor
|
|
132
|
+
StringConvertor.regex = '[^/]*'
|
|
133
|
+
|
|
134
|
+
@patch
|
|
135
|
+
def to_string(self: StringConvertor, value: str) -> str:
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
@patch
|
|
139
|
+
def url_path_for(self: HTTPConnection, name: str, **path_params):
|
|
140
|
+
...
|
|
141
|
+
_verbs = dict(get='hx-get', post='hx-post', put='hx-post', delete='hx-delete', patch='hx-patch', link='href')
|
|
142
|
+
|
|
143
|
+
def _url_for(req, t):
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
def _find_targets(req, resp):
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def _apply_ft(o):
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
def _to_xml(req, resp, indent):
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
def _xt_resp(req, resp):
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
def _resp(req, resp, cls=empty):
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
async def _wrap_call(f, req, params):
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
class RouteX(Route):
|
|
165
|
+
|
|
166
|
+
def __init__(self, path: str, endpoint, *, methods=None, name=None, include_in_schema=True, middleware=None, hdrs=None, ftrs=None, before=None, after=None, htmlkw=None, **bodykw):
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
async def _endp(self, req):
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
class RouterX(Router):
|
|
173
|
+
|
|
174
|
+
def __init__(self, routes=None, redirect_slashes=True, default=None, on_startup=None, on_shutdown=None, lifespan=None, *, middleware=None, hdrs=None, ftrs=None, before=None, after=None, htmlkw=None, **bodykw):
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
def add_route(self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True):
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
def add_ws(self, path: str, recv: callable, conn: callable=None, disconn: callable=None, name=None):
|
|
181
|
+
...
|
|
182
|
+
htmxscr = Script(src='https://unpkg.com/htmx.org@next/dist/htmx.min.js')
|
|
183
|
+
htmxwsscr = Script(src='https://unpkg.com/htmx-ext-ws/ws.js')
|
|
184
|
+
surrsrc = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js')
|
|
185
|
+
scopesrc = Script(src='https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js')
|
|
186
|
+
viewport = Meta(name='viewport', content='width=device-width, initial-scale=1, viewport-fit=cover')
|
|
187
|
+
charset = Meta(charset='utf-8')
|
|
188
|
+
|
|
189
|
+
def get_key(key=None, fname='.sesskey'):
|
|
190
|
+
...
|
|
191
|
+
|
|
192
|
+
def _list(o):
|
|
193
|
+
...
|
|
194
|
+
|
|
195
|
+
def _wrap_ex(f, hdrs, ftrs, htmlkw, bodykw):
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
class FastHTML(Starlette):
|
|
199
|
+
|
|
200
|
+
def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None, on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, before=None, after=None, ws_hdr=False, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware, secret_key=None, session_cookie='session_', max_age=365 * 24 * 3600, sess_path='/', same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', htmlkw=None, **bodykw):
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
def ws(self, path: str, conn=None, disconn=None, name=None):
|
|
204
|
+
...
|
|
205
|
+
|
|
206
|
+
def _mk_locfunc(f, p):
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
@patch
|
|
210
|
+
def route(self: FastHTML, path: str=None, methods=None, name=None, include_in_schema=True):
|
|
211
|
+
"""Add a route at `path`; the function name is the default method"""
|
|
212
|
+
...
|
|
213
|
+
all_meths = 'get post put delete patch head trace options'.split()
|
|
214
|
+
for o in all_meths:
|
|
215
|
+
setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
|
|
216
|
+
|
|
217
|
+
def serve(appname=None, app='app', host='0.0.0.0', port=None, reload=True, reload_includes: list[str] | str | None=None, reload_excludes: list[str] | str | None=None):
|
|
218
|
+
"""Run the app in an async server, with live reload set as the default."""
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
def cookie(key: str, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite='lax'):
|
|
222
|
+
"""Create a 'set-cookie' `HttpHeader`"""
|
|
223
|
+
...
|
|
224
|
+
|
|
225
|
+
def reg_re_param(m, s):
|
|
226
|
+
...
|
|
227
|
+
reg_re_param('path', '.*?')
|
|
228
|
+
reg_re_param('static', 'ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html')
|
|
229
|
+
|
|
230
|
+
class MiddlewareBase:
|
|
231
|
+
|
|
232
|
+
async def __call__(self, scope, receive, send) -> None:
|
|
233
|
+
...
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/08_oauth.ipynb.
|
|
2
|
+
|
|
3
|
+
# %% auto 0
|
|
4
|
+
__all__ = ['GoogleAppClient', 'GitHubAppClient', 'HuggingFaceClient', 'DiscordAppClient']
|
|
5
|
+
|
|
6
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
7
|
+
from .common import *
|
|
8
|
+
from oauthlib.oauth2 import WebApplicationClient
|
|
9
|
+
from urllib.parse import urlencode, parse_qs, quote, unquote
|
|
10
|
+
from httpx import get, post
|
|
11
|
+
import secrets
|
|
12
|
+
|
|
13
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
14
|
+
class _AppClient(WebApplicationClient):
|
|
15
|
+
def __init__(self, client_id, client_secret, redirect_uri, code=None, scope=None, **kwargs):
|
|
16
|
+
super().__init__(client_id, code=code, scope=scope, **kwargs)
|
|
17
|
+
self.client_secret,self.redirect_uri = client_secret,redirect_uri
|
|
18
|
+
|
|
19
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
20
|
+
class GoogleAppClient(_AppClient):
|
|
21
|
+
"A `WebApplicationClient` for Google oauth2"
|
|
22
|
+
base_url = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
23
|
+
token_url = "https://www.googleapis.com/oauth2/v4/token"
|
|
24
|
+
info_url = "https://www.googleapis.com/oauth2/v3/userinfo"
|
|
25
|
+
id_key = 'sub'
|
|
26
|
+
|
|
27
|
+
def __init__(self, client_id, client_secret, redirect_uri=None, redirect_uris=None, code=None, scope=None, **kwargs):
|
|
28
|
+
if redirect_uris and not redirect_uri: redirect_uri = redirect_uris[0]
|
|
29
|
+
scope_pre = "https://www.googleapis.com/auth/userinfo"
|
|
30
|
+
if not scope: scope=["openid", f"{scope_pre}.email", f"{scope_pre}.profile"]
|
|
31
|
+
super().__init__(client_id, client_secret, redirect_uri, code=code, scope=scope, **kwargs)
|
|
32
|
+
|
|
33
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
34
|
+
class GitHubAppClient(_AppClient):
|
|
35
|
+
"A `WebApplicationClient` for GitHub oauth2"
|
|
36
|
+
base_url = "https://github.com/login/oauth/authorize"
|
|
37
|
+
token_url = "https://github.com/login/oauth/access_token"
|
|
38
|
+
info_url = "https://api.github.com/user"
|
|
39
|
+
id_key = 'id'
|
|
40
|
+
|
|
41
|
+
def __init__(self, client_id, client_secret, redirect_uri, code=None, scope=None, **kwargs):
|
|
42
|
+
if not scope: scope="user"
|
|
43
|
+
super().__init__(client_id, client_secret, redirect_uri, code=code, scope=scope, **kwargs)
|
|
44
|
+
|
|
45
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
46
|
+
class HuggingFaceClient(_AppClient):
|
|
47
|
+
"A `WebApplicationClient` for HuggingFace oauth2"
|
|
48
|
+
|
|
49
|
+
base_url = "https://huggingface.co/oauth/authorize"
|
|
50
|
+
token_url = "https://huggingface.co/oauth/token"
|
|
51
|
+
info_url = "https://huggingface.co/oauth/userinfo"
|
|
52
|
+
id_key = 'sub'
|
|
53
|
+
|
|
54
|
+
def __init__(self, client_id, client_secret, redirect_uri=None, redirect_uris=None, code=None, scope=None, state=None, **kwargs):
|
|
55
|
+
if redirect_uris and not redirect_uri: redirect_uri = redirect_uris[0]
|
|
56
|
+
if not scope: scope=["openid","profile"]
|
|
57
|
+
if not state: state=secrets.token_urlsafe(16)
|
|
58
|
+
super().__init__(client_id, client_secret, redirect_uri, code=code, scope=scope, state=state, **kwargs)
|
|
59
|
+
|
|
60
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
61
|
+
class DiscordAppClient(_AppClient):
|
|
62
|
+
"A `WebApplicationClient` for Discord oauth2"
|
|
63
|
+
base_url = "https://discord.com/oauth2/authorize"
|
|
64
|
+
token_url = "https://discord.com/api/oauth2/token"
|
|
65
|
+
revoke_url = "https://discord.com/api/oauth2/token/revoke"
|
|
66
|
+
id_key = 'id'
|
|
67
|
+
|
|
68
|
+
def __init__(self, client_id, client_secret, redirect_uri, is_user=False, perms=0, scope=None, **kwargs):
|
|
69
|
+
if not scope: scope="applications.commands applications.commands.permissions.update identify"
|
|
70
|
+
self.integration_type = 1 if is_user else 0
|
|
71
|
+
self.perms = perms
|
|
72
|
+
super().__init__(client_id, client_secret, redirect_uri, scope=scope, **kwargs)
|
|
73
|
+
|
|
74
|
+
def login_link(self):
|
|
75
|
+
d = dict(response_type='code', client_id=self.client_id,
|
|
76
|
+
integration_type=self.integration_type, scope=self.scope,
|
|
77
|
+
redirect_uri=self.redirect_uri) #, permissions=self.perms, prompt='consent')
|
|
78
|
+
return f'{self.base_url}?' + urlencode(d)
|
|
79
|
+
|
|
80
|
+
def parse_response(self, code):
|
|
81
|
+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
82
|
+
data = dict(grant_type='authorization_code', code=code, redirect_uri=self.redirect_uri)
|
|
83
|
+
r = post(self.token_url, data=data, headers=headers, auth=(self.client_id, self.client_secret))
|
|
84
|
+
r.raise_for_status()
|
|
85
|
+
self.parse_request_body_response(r.text)
|
|
86
|
+
|
|
87
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
88
|
+
@patch
|
|
89
|
+
def login_link(self:WebApplicationClient, scope=None):
|
|
90
|
+
"Get a login link for this client"
|
|
91
|
+
if not scope: scope=self.scope
|
|
92
|
+
return self.prepare_request_uri(self.base_url, self.redirect_uri, scope)
|
|
93
|
+
|
|
94
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
95
|
+
@patch
|
|
96
|
+
def login_link_with_state(self:WebApplicationClient, scope=None, state=None):
|
|
97
|
+
"Get a login link for this client"
|
|
98
|
+
if not scope: scope=self.scope
|
|
99
|
+
if not state: state=self.state
|
|
100
|
+
return self.prepare_request_uri(self.base_url, self.redirect_uri, scope, state)
|
|
101
|
+
|
|
102
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
103
|
+
@patch
|
|
104
|
+
def parse_response(self:_AppClient, code):
|
|
105
|
+
"Get the token from the oauth2 server response"
|
|
106
|
+
payload = dict(code=code, redirect_uri=self.redirect_uri, client_id=self.client_id,
|
|
107
|
+
client_secret=self.client_secret, grant_type='authorization_code')
|
|
108
|
+
r = post(self.token_url, json=payload)
|
|
109
|
+
r.raise_for_status()
|
|
110
|
+
self.parse_request_body_response(r.text)
|
|
111
|
+
|
|
112
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
113
|
+
@patch
|
|
114
|
+
def get_info(self:_AppClient):
|
|
115
|
+
"Get the info for authenticated user"
|
|
116
|
+
headers = {'Authorization': f'Bearer {self.token["access_token"]}'}
|
|
117
|
+
return get(self.info_url, headers=headers).json()
|
|
118
|
+
|
|
119
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
120
|
+
@patch
|
|
121
|
+
def retr_info(self:_AppClient, code):
|
|
122
|
+
"Combines `parse_response` and `get_info`"
|
|
123
|
+
self.parse_response(code)
|
|
124
|
+
return self.get_info()
|
|
125
|
+
|
|
126
|
+
# %% ../nbs/api/08_oauth.ipynb
|
|
127
|
+
@patch
|
|
128
|
+
def retr_id(self:_AppClient, code):
|
|
129
|
+
"Call `retr_info` and then return id/subscriber value"
|
|
130
|
+
return self.retr_info(code)[self.id_key]
|