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.
Files changed (40) hide show
  1. {python-fasthtml-0.4.2/python_fasthtml.egg-info → python-fasthtml-0.4.4}/PKG-INFO +2 -2
  2. python-fasthtml-0.4.4/fasthtml/__init__.py +2 -0
  3. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/_modidx.py +31 -24
  4. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/common.py +1 -0
  5. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/core.py +39 -42
  6. python-fasthtml-0.4.4/fasthtml/core.pyi +233 -0
  7. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/fastapp.py +1 -0
  8. python-fasthtml-0.4.4/fasthtml/oauth.py +130 -0
  9. python-fasthtml-0.4.4/fasthtml/pico.py +83 -0
  10. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/xtend.py +7 -74
  11. python-fasthtml-0.4.4/fasthtml/xtend.pyi +107 -0
  12. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4/python_fasthtml.egg-info}/PKG-INFO +2 -2
  13. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/SOURCES.txt +2 -0
  14. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/requires.txt +1 -1
  15. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/settings.ini +2 -2
  16. python-fasthtml-0.4.2/fasthtml/__init__.py +0 -2
  17. python-fasthtml-0.4.2/fasthtml/oauth.py +0 -94
  18. python-fasthtml-0.4.2/fasthtml/xtend.pyi +0 -138
  19. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/CONTRIBUTING.md +0 -0
  20. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/LICENSE +0 -0
  21. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/MANIFEST.in +0 -0
  22. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/README.md +0 -0
  23. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/authmw.py +0 -0
  24. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/basics.py +0 -0
  25. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/cli.py +0 -0
  26. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/components.py +0 -0
  27. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/components.pyi +0 -0
  28. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/ft.py +0 -0
  29. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/js.py +0 -0
  30. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/katex.js +0 -0
  31. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/live_reload.py +0 -0
  32. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/starlette.py +0 -0
  33. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/svg.py +0 -0
  34. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/fasthtml/toaster.py +0 -0
  35. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  36. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/entry_points.txt +0 -0
  37. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/not-zip-safe +0 -0
  38. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/python_fasthtml.egg-info/top_level.txt +0 -0
  39. {python-fasthtml-0.4.2 → python-fasthtml-0.4.4}/setup.cfg +0 -0
  40. {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.2
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.8
25
+ Requires-Dist: fastlite>=0.0.9
26
26
  Requires-Dist: python-multipart
27
27
  Requires-Dist: beautifulsoup4
28
28
  Provides-Extra: dev
@@ -0,0 +1,2 @@
1
+ __version__ = "0.4.4"
2
+ from .core import *
@@ -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.GitHubAppClient': ('incomplete/oauth.html#githubappclient', 'fasthtml/oauth.py'),
102
- 'fasthtml.oauth.GitHubAppClient.__init__': ( 'incomplete/oauth.html#githubappclient.__init__',
103
- 'fasthtml/oauth.py'),
104
- 'fasthtml.oauth.GoogleAppClient': ('incomplete/oauth.html#googleappclient', 'fasthtml/oauth.py'),
105
- 'fasthtml.oauth.GoogleAppClient.__init__': ( 'incomplete/oauth.html#googleappclient.__init__',
106
- 'fasthtml/oauth.py'),
107
- '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',
108
114
  'fasthtml/oauth.py'),
109
- 'fasthtml.oauth._AppClient': ('incomplete/oauth.html#_appclient', 'fasthtml/oauth.py'),
110
- 'fasthtml.oauth._AppClient.__init__': ('incomplete/oauth.html#_appclient.__init__', 'fasthtml/oauth.py'),
111
- 'fasthtml.oauth._AppClient.get_info': ('incomplete/oauth.html#_appclient.get_info', 'fasthtml/oauth.py'),
112
- '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',
113
121
  'fasthtml/oauth.py'),
114
- 'fasthtml.oauth._AppClient.retr_id': ('incomplete/oauth.html#_appclient.retr_id', 'fasthtml/oauth.py'),
115
- 'fasthtml.oauth._AppClient.retr_info': ('incomplete/oauth.html#_appclient.retr_info', 'fasthtml/oauth.py'),
116
- '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')},
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')}}}
@@ -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 *
@@ -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', 'cookie',
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
- 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}
@@ -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
- class _SessionMiddleware(SessionMiddleware):
419
- "Same as Starlette's `SessionMiddleware`, but wraps `session` in an AttrDict"
420
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
421
- if scope["type"] not in ("http", "websocket"):
422
- await self.app(scope, receive, send)
423
- return
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=_SessionMiddleware,
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
- # %% ../nbs/api/00_core.ipynb
468
- def _mk_locfunc(f,p):
469
- class _lf:
470
- def __init__(self): update_wrapper(self, f)
471
- def __call__(self, **kw): return p + (f'?{urlencode(kw)}' if kw else '')
472
- def __str__(self): return p
473
- return _lf()
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
+ ...
@@ -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]