python-fasthtml 0.4.3__tar.gz → 0.4.5__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 (40) hide show
  1. {python_fasthtml-0.4.3/python_fasthtml.egg-info → python-fasthtml-0.4.5}/PKG-INFO +1 -1
  2. python-fasthtml-0.4.5/fasthtml/__init__.py +2 -0
  3. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/_modidx.py +31 -21
  4. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/cli.py +4 -3
  5. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/common.py +1 -0
  6. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/components.py +1 -1
  7. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/core.py +34 -17
  8. python-fasthtml-0.4.5/fasthtml/core.pyi +238 -0
  9. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/fastapp.py +1 -0
  10. python-fasthtml-0.4.5/fasthtml/oauth.py +130 -0
  11. python-fasthtml-0.4.5/fasthtml/pico.py +83 -0
  12. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/xtend.py +7 -74
  13. python-fasthtml-0.4.5/fasthtml/xtend.pyi +107 -0
  14. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5/python_fasthtml.egg-info}/PKG-INFO +1 -1
  15. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/python_fasthtml.egg-info/SOURCES.txt +2 -0
  16. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/settings.ini +1 -1
  17. python_fasthtml-0.4.3/fasthtml/__init__.py +0 -2
  18. python_fasthtml-0.4.3/fasthtml/oauth.py +0 -94
  19. python_fasthtml-0.4.3/fasthtml/xtend.pyi +0 -138
  20. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/CONTRIBUTING.md +0 -0
  21. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/LICENSE +0 -0
  22. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/MANIFEST.in +0 -0
  23. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/README.md +0 -0
  24. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/authmw.py +0 -0
  25. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/basics.py +0 -0
  26. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/components.pyi +0 -0
  27. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/ft.py +0 -0
  28. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/js.py +0 -0
  29. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/katex.js +0 -0
  30. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/live_reload.py +0 -0
  31. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/starlette.py +0 -0
  32. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/svg.py +0 -0
  33. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/fasthtml/toaster.py +0 -0
  34. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  35. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/python_fasthtml.egg-info/entry_points.txt +0 -0
  36. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/python_fasthtml.egg-info/not-zip-safe +0 -0
  37. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/python_fasthtml.egg-info/requires.txt +0 -0
  38. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/python_fasthtml.egg-info/top_level.txt +0 -0
  39. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/setup.cfg +0 -0
  40. {python_fasthtml-0.4.3 → python-fasthtml-0.4.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.4.3
3
+ Version: 0.4.5
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
@@ -0,0 +1,2 @@
1
+ __version__ = "0.4.5"
2
+ from .core import *
@@ -74,6 +74,7 @@ d = { 'settings': { 'branch': 'main',
74
74
  'fasthtml.core.cookie': ('api/core.html#cookie', 'fasthtml/core.py'),
75
75
  'fasthtml.core.date': ('api/core.html#date', 'fasthtml/core.py'),
76
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'),
77
78
  'fasthtml.core.flat_xt': ('api/core.html#flat_xt', 'fasthtml/core.py'),
78
79
  'fasthtml.core.form2dict': ('api/core.html#form2dict', 'fasthtml/core.py'),
79
80
  'fasthtml.core.get_key': ('api/core.html#get_key', 'fasthtml/core.py'),
@@ -95,22 +96,38 @@ d = { 'settings': { 'branch': 'main',
95
96
  'fasthtml.js.dark_media': ('api/js.html#dark_media', 'fasthtml/js.py'),
96
97
  'fasthtml.js.light_media': ('api/js.html#light_media', 'fasthtml/js.py')},
97
98
  'fasthtml.live_reload': {},
98
- 'fasthtml.oauth': { 'fasthtml.oauth.GitHubAppClient': ('incomplete/oauth.html#githubappclient', 'fasthtml/oauth.py'),
99
- 'fasthtml.oauth.GitHubAppClient.__init__': ( 'incomplete/oauth.html#githubappclient.__init__',
100
- 'fasthtml/oauth.py'),
101
- 'fasthtml.oauth.GoogleAppClient': ('incomplete/oauth.html#googleappclient', 'fasthtml/oauth.py'),
102
- 'fasthtml.oauth.GoogleAppClient.__init__': ( 'incomplete/oauth.html#googleappclient.__init__',
103
- 'fasthtml/oauth.py'),
104
- 'fasthtml.oauth.WebApplicationClient.login_link': ( 'incomplete/oauth.html#webapplicationclient.login_link',
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',
105
114
  'fasthtml/oauth.py'),
106
- 'fasthtml.oauth._AppClient': ('incomplete/oauth.html#_appclient', 'fasthtml/oauth.py'),
107
- 'fasthtml.oauth._AppClient.__init__': ('incomplete/oauth.html#_appclient.__init__', 'fasthtml/oauth.py'),
108
- 'fasthtml.oauth._AppClient.get_info': ('incomplete/oauth.html#_appclient.get_info', 'fasthtml/oauth.py'),
109
- 'fasthtml.oauth._AppClient.parse_response': ( 'incomplete/oauth.html#_appclient.parse_response',
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',
110
121
  'fasthtml/oauth.py'),
111
- 'fasthtml.oauth._AppClient.retr_id': ('incomplete/oauth.html#_appclient.retr_id', 'fasthtml/oauth.py'),
112
- 'fasthtml.oauth._AppClient.retr_info': ('incomplete/oauth.html#_appclient.retr_info', 'fasthtml/oauth.py'),
113
- 'fasthtml.oauth.retr_code': ('incomplete/oauth.html#retr_code', 'fasthtml/oauth.py')},
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')},
114
131
  'fasthtml.starlette': {},
115
132
  'fasthtml.svg': {},
116
133
  'fasthtml.toaster': {},
@@ -118,21 +135,15 @@ d = { 'settings': { 'branch': 'main',
118
135
  'fasthtml.xtend.AX': ('api/xtend.html#ax', 'fasthtml/xtend.py'),
119
136
  'fasthtml.xtend.Any': ('api/xtend.html#any', 'fasthtml/xtend.py'),
120
137
  'fasthtml.xtend.AnyNow': ('api/xtend.html#anynow', 'fasthtml/xtend.py'),
121
- 'fasthtml.xtend.Card': ('api/xtend.html#card', 'fasthtml/xtend.py'),
122
138
  'fasthtml.xtend.CheckboxX': ('api/xtend.html#checkboxx', 'fasthtml/xtend.py'),
123
- 'fasthtml.xtend.Container': ('api/xtend.html#container', 'fasthtml/xtend.py'),
124
- 'fasthtml.xtend.DialogX': ('api/xtend.html#dialogx', 'fasthtml/xtend.py'),
125
139
  'fasthtml.xtend.Favicon': ('api/xtend.html#favicon', 'fasthtml/xtend.py'),
126
140
  'fasthtml.xtend.Form': ('api/xtend.html#form', 'fasthtml/xtend.py'),
127
- 'fasthtml.xtend.Grid': ('api/xtend.html#grid', 'fasthtml/xtend.py'),
128
- 'fasthtml.xtend.Group': ('api/xtend.html#group', 'fasthtml/xtend.py'),
129
141
  'fasthtml.xtend.Hidden': ('api/xtend.html#hidden', 'fasthtml/xtend.py'),
130
142
  'fasthtml.xtend.Now': ('api/xtend.html#now', 'fasthtml/xtend.py'),
131
143
  'fasthtml.xtend.On': ('api/xtend.html#on', 'fasthtml/xtend.py'),
132
144
  'fasthtml.xtend.Prev': ('api/xtend.html#prev', 'fasthtml/xtend.py'),
133
145
  'fasthtml.xtend.Script': ('api/xtend.html#script', 'fasthtml/xtend.py'),
134
146
  'fasthtml.xtend.ScriptX': ('api/xtend.html#scriptx', 'fasthtml/xtend.py'),
135
- 'fasthtml.xtend.Search': ('api/xtend.html#search', 'fasthtml/xtend.py'),
136
147
  'fasthtml.xtend.Socials': ('api/xtend.html#socials', 'fasthtml/xtend.py'),
137
148
  'fasthtml.xtend.Style': ('api/xtend.html#style', 'fasthtml/xtend.py'),
138
149
  'fasthtml.xtend.StyleX': ('api/xtend.html#stylex', 'fasthtml/xtend.py'),
@@ -143,5 +154,4 @@ d = { 'settings': { 'branch': 'main',
143
154
  'fasthtml.xtend.loose_format': ('api/xtend.html#loose_format', 'fasthtml/xtend.py'),
144
155
  'fasthtml.xtend.replace_css_vars': ('api/xtend.html#replace_css_vars', 'fasthtml/xtend.py'),
145
156
  'fasthtml.xtend.run_js': ('api/xtend.html#run_js', 'fasthtml/xtend.py'),
146
- 'fasthtml.xtend.set_pico_cls': ('api/xtend.html#set_pico_cls', 'fasthtml/xtend.py'),
147
157
  'fasthtml.xtend.undouble_braces': ('api/xtend.html#undouble_braces', 'fasthtml/xtend.py')}}}
@@ -39,9 +39,10 @@ def railway_deploy(
39
39
  assert nm=='railwayapp', f'Unexpected railway version string: {nm}'
40
40
  if ver2tuple(ver)<(3,8): return print("Please update your railway CLI version to 3.8 or higher")
41
41
  cp = run("railway status --json".split(), capture_output=True)
42
- if not cp.returncode: print("Checking deployed projects...")
43
- project_name = json.loads(cp.stdout.decode()).get('name')
44
- if project_name == name: return print("This project is already deployed. Run `railway open`.")
42
+ if not cp.returncode:
43
+ print("Checking deployed projects...")
44
+ project_name = json.loads(cp.stdout.decode()).get('name')
45
+ if project_name == name: return print("This project is already deployed. Run `railway open`.")
45
46
  reqs = Path('requirements.txt')
46
47
  if not reqs.exists(): reqs.write_text('python-fasthtml')
47
48
  _run(f"railway init -n {name}".split())
@@ -7,6 +7,7 @@ from fastcore.xml import *
7
7
  from sqlite_minutils import Database
8
8
  from fastlite import *
9
9
  from .basics import *
10
+ from .pico import *
10
11
  from .authmw import *
11
12
  from .live_reload import *
12
13
  from .toaster import *
@@ -146,7 +146,7 @@ def html2ft(html, attr1st=False):
146
146
  def _parse(elm, lvl=0, indent=4):
147
147
  if isinstance(elm, str): return repr(elm.strip()) if elm.strip() else ''
148
148
  if isinstance(elm, list): return '\n'.join(_parse(o, lvl) for o in elm)
149
- tag_name = elm.name.capitalize()
149
+ tag_name = elm.name.capitalize().replace("-", "_")
150
150
  if tag_name=='[document]': return _parse(list(elm.children), lvl)
151
151
  cts = elm.contents
152
152
  cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)
@@ -1,10 +1,10 @@
1
1
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.
2
2
 
3
3
  # %% auto 0
4
- __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmxscr', 'htmxwsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths',
5
- 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', 'form2dict', 'flat_xt', 'Beforeware',
6
- 'WS_RouteX', 'uri', 'decode_uri', 'RouteX', 'RouterX', 'get_key', 'FastHTML', 'serve', 'cookie',
7
- 'reg_re_param', 'MiddlewareBase']
4
+ __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmxscr', 'htmxwsscr', 'fhjsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset',
5
+ 'all_meths', 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', 'form2dict', 'flat_xt',
6
+ 'Beforeware', 'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', 'RouteX', 'RouterX', 'get_key', 'FastHTML',
7
+ 'serve', '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
- if not isinstance(resp, tuple): resp = (resp,)
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}
@@ -389,6 +401,7 @@ class RouterX(Router):
389
401
  # %% ../nbs/api/00_core.ipynb
390
402
  htmxscr = Script(src="https://unpkg.com/htmx.org@next/dist/htmx.min.js")
391
403
  htmxwsscr = Script(src="https://unpkg.com/htmx-ext-ws/ws.js")
404
+ fhjsscr = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@main/fasthtml.js")
392
405
  surrsrc = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js")
393
406
  scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js")
394
407
  viewport = Meta(name="viewport", content="width=device-width, initial-scale=1, viewport-fit=cover")
@@ -414,6 +427,15 @@ def _wrap_ex(f, hdrs, ftrs, htmlkw, bodykw):
414
427
  return _resp(req, res)
415
428
  return _f
416
429
 
430
+ # %% ../nbs/api/00_core.ipynb
431
+ def _mk_locfunc(f,p):
432
+ class _lf:
433
+ def __init__(self): update_wrapper(self, f)
434
+ def __call__(self, *args, **kw): return f(*args, **kw)
435
+ def rt(self, **kw): return p + (f'?{urlencode(kw)}' if kw else '')
436
+ def __str__(self): return p
437
+ return _lf()
438
+
417
439
  # %% ../nbs/api/00_core.ipynb
418
440
  class FastHTML(Starlette):
419
441
  def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
@@ -435,7 +457,7 @@ class FastHTML(Starlette):
435
457
  if default_hdrs:
436
458
  if surreal: hdrs = [surrsrc,scopesrc] + hdrs
437
459
  if ws_hdr: hdrs = [htmxwsscr] + hdrs
438
- if htmx: hdrs = [htmxscr] + hdrs
460
+ if htmx: hdrs = [htmxscr,fhjsscr] + hdrs
439
461
  hdrs = [charset, viewport] + hdrs
440
462
  excs = {k:_wrap_ex(v, hdrs, ftrs, htmlkw, bodykw) for k,v in (exception_handlers or {}).items()}
441
463
  super().__init__(debug, routes, middleware, excs, on_startup, on_shutdown, lifespan=lifespan)
@@ -443,29 +465,24 @@ class FastHTML(Starlette):
443
465
  hdrs=hdrs, ftrs=ftrs, before=before, after=after, htmlkw=htmlkw, **bodykw)
444
466
 
445
467
  def ws(self, path:str, conn=None, disconn=None, name=None):
468
+ "Add a websocket route at `path`"
446
469
  def f(func):
447
470
  self.router.add_ws(path, func, conn=conn, disconn=disconn, name=name)
448
471
  return func
449
472
  return f
450
473
 
451
474
  # %% ../nbs/api/00_core.ipynb
452
- def _mk_locfunc(f,p):
453
- class _lf:
454
- def __init__(self): update_wrapper(self, f)
455
- def __call__(self, **kw): return p + (f'?{urlencode(kw)}' if kw else '')
456
- def __str__(self): return p
457
- return _lf()
475
+ all_meths = 'get post put delete patch head trace options'.split()
458
476
 
459
- # %% ../nbs/api/00_core.ipynb
460
477
  @patch
461
478
  def route(self:FastHTML, path:str=None, methods=None, name=None, include_in_schema=True):
462
- "Add a route at `path`; the function name is the default method"
479
+ "Add a route at `path`"
463
480
  pathstr = None if callable(path) else path
464
481
  def f(func):
465
482
  n,fn,p = name,func.__name__,pathstr
466
- assert path or (fn not in _verbs), "Must provide a path when using http verb-based function name"
483
+ assert path or (fn not in all_meths), "Must provide a path when using http verb-based function name"
467
484
  if methods: m = [methods] if isinstance(methods,str) else methods
468
- else: m = [fn] if fn in _verbs else ['get'] if fn=='index' else ['post']
485
+ else: m = [fn] if fn in all_meths else ['get','post']
469
486
  if not n: n = fn
470
487
  if not p: p = '/'+('' if fn=='index' else fn)
471
488
  self.router.add_route(p, func, methods=m, name=n, include_in_schema=include_in_schema)
@@ -474,7 +491,7 @@ def route(self:FastHTML, path:str=None, methods=None, name=None, include_in_sche
474
491
  return lf
475
492
  return f(path) if callable(path) else f
476
493
 
477
- all_meths = 'get post put delete patch head trace options'.split()
494
+ # %% ../nbs/api/00_core.ipynb
478
495
  for o in all_meths: setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
479
496
 
480
497
  # %% ../nbs/api/00_core.ipynb
@@ -0,0 +1,238 @@
1
+ __all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmxscr', 'htmxwsscr', 'fhjsscr', 'surrsrc', 'scopesrc', 'viewport', 'charset', 'all_meths', 'date', 'snake2hyphens', 'HtmxHeaders', 'str2int', 'HttpHeader', 'form2dict', 'flat_xt', 'Beforeware', 'WS_RouteX', 'uri', 'decode_uri', 'flat_tuple', '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 flat_tuple(o):
156
+ """Flatten lists"""
157
+ ...
158
+
159
+ def _xt_resp(req, resp):
160
+ ...
161
+
162
+ def _resp(req, resp, cls=empty):
163
+ ...
164
+
165
+ async def _wrap_call(f, req, params):
166
+ ...
167
+
168
+ class RouteX(Route):
169
+
170
+ 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):
171
+ ...
172
+
173
+ async def _endp(self, req):
174
+ ...
175
+
176
+ class RouterX(Router):
177
+
178
+ 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):
179
+ ...
180
+
181
+ def add_route(self, path: str, endpoint: callable, methods=None, name=None, include_in_schema=True):
182
+ ...
183
+
184
+ def add_ws(self, path: str, recv: callable, conn: callable=None, disconn: callable=None, name=None):
185
+ ...
186
+ htmxscr = Script(src='https://unpkg.com/htmx.org@next/dist/htmx.min.js')
187
+ htmxwsscr = Script(src='https://unpkg.com/htmx-ext-ws/ws.js')
188
+ fhjsscr = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@main/fasthtml.js')
189
+ surrsrc = Script(src='https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js')
190
+ scopesrc = Script(src='https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js')
191
+ viewport = Meta(name='viewport', content='width=device-width, initial-scale=1, viewport-fit=cover')
192
+ charset = Meta(charset='utf-8')
193
+
194
+ def get_key(key=None, fname='.sesskey'):
195
+ ...
196
+
197
+ def _list(o):
198
+ ...
199
+
200
+ def _wrap_ex(f, hdrs, ftrs, htmlkw, bodykw):
201
+ ...
202
+
203
+ def _mk_locfunc(f, p):
204
+ ...
205
+
206
+ class FastHTML(Starlette):
207
+
208
+ 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):
209
+ ...
210
+
211
+ def ws(self, path: str, conn=None, disconn=None, name=None):
212
+ """Add a websocket route at `path`"""
213
+ ...
214
+
215
+ def route(self, path: str=None, methods=None, name=None, include_in_schema=True):
216
+ """Add a route at `path`"""
217
+ ...
218
+ all_meths = 'get post put delete patch head trace options'.split()
219
+ for o in all_meths:
220
+ setattr(FastHTML, o, partialmethod(FastHTML.route, methods=o))
221
+
222
+ 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):
223
+ """Run the app in an async server, with live reload set as the default."""
224
+ ...
225
+
226
+ def cookie(key: str, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite='lax'):
227
+ """Create a 'set-cookie' `HttpHeader`"""
228
+ ...
229
+
230
+ def reg_re_param(m, s):
231
+ ...
232
+ reg_re_param('path', '.*?')
233
+ reg_re_param('static', 'ico|gif|jpg|jpeg|webm|css|js|woff|png|svg|mp4|webp|ttf|otf|eot|woff2|txt|html')
234
+
235
+ class MiddlewareBase:
236
+
237
+ async def __call__(self, scope, receive, send) -> None:
238
+ ...
@@ -7,6 +7,7 @@ import inspect,uvicorn
7
7
  from fastcore.utils import *
8
8
  from fastlite import *
9
9
  from .basics import *
10
+ from .pico import *
10
11
  from .starlette import *
11
12
  from .live_reload import FastHTMLWithLiveReload
12
13
 
@@ -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]