fastlifeweb 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- fastlife/__init__.py +5 -0
- fastlife/configurator/configurator.py +8 -2
- fastlife/configurator/registry.py +7 -3
- fastlife/configurator/settings.py +1 -1
- fastlife/request/form_data.py +2 -7
- fastlife/security/csrf.py +1 -1
- fastlife/session/middleware.py +0 -1
- fastlife/shared_utils/__init__.py +0 -0
- fastlife/shared_utils/resolver.py +5 -3
- fastlife/templates/A.jinja +5 -0
- fastlife/templates/Button.jinja +27 -0
- fastlife/templates/Checkbox.jinja +2 -0
- fastlife/templates/CsrfToken.jinja +2 -0
- fastlife/templates/Form.jinja +5 -0
- fastlife/templates/H1.jinja +4 -0
- fastlife/templates/H2.jinja +4 -0
- fastlife/templates/Hidden.jinja +6 -0
- fastlife/templates/Input.jinja +8 -0
- fastlife/templates/Label.jinja +3 -0
- fastlife/templates/Option.jinja +2 -0
- fastlife/templates/Radio.jinja +8 -0
- fastlife/templates/Select.jinja +8 -0
- fastlife/templates/__init__.py +0 -0
- fastlife/templates/pydantic_form/Boolean.jinja +7 -0
- fastlife/templates/pydantic_form/Dropdown.jinja +17 -0
- fastlife/templates/pydantic_form/Hidden.jinja +2 -0
- fastlife/templates/pydantic_form/Model.jinja +16 -0
- fastlife/templates/pydantic_form/Sequence.jinja +37 -0
- fastlife/templates/pydantic_form/Text.jinja +12 -0
- fastlife/templates/pydantic_form/Union.jinja +26 -0
- fastlife/templates/pydantic_form/Widget.jinja +10 -0
- fastlife/templating/__init__.py +2 -2
- fastlife/templating/binding.py +6 -7
- fastlife/templating/renderer/__init__.py +2 -2
- fastlife/templating/renderer/abstract.py +9 -7
- fastlife/templating/renderer/jinjax.py +73 -0
- fastlife/templating/renderer/widgets/base.py +14 -13
- fastlife/templating/renderer/widgets/boolean.py +1 -1
- fastlife/templating/renderer/widgets/dropdown.py +7 -6
- fastlife/templating/renderer/widgets/factory.py +14 -7
- fastlife/templating/renderer/widgets/hidden.py +1 -1
- fastlife/templating/renderer/widgets/model.py +8 -10
- fastlife/templating/renderer/widgets/sequence.py +4 -4
- fastlife/templating/renderer/widgets/text.py +1 -1
- fastlife/templating/renderer/widgets/union.py +5 -4
- fastlife/testing/testclient.py +115 -10
- fastlife/views/pydantic_form.py +8 -3
- {fastlifeweb-0.2.2.dist-info → fastlifeweb-0.3.0.dist-info}/METADATA +2 -2
- fastlifeweb-0.3.0.dist-info/RECORD +63 -0
- {fastlifeweb-0.2.2.dist-info → fastlifeweb-0.3.0.dist-info}/WHEEL +1 -1
- fastlife/templates/base.jinja2 +0 -2
- fastlife/templates/globals.jinja2 +0 -83
- fastlife/templates/pydantic_form/boolean.jinja2 +0 -8
- fastlife/templates/pydantic_form/dropdown.jinja2 +0 -18
- fastlife/templates/pydantic_form/hidden.jinja2 +0 -1
- fastlife/templates/pydantic_form/model.jinja2 +0 -16
- fastlife/templates/pydantic_form/sequence.jinja2 +0 -41
- fastlife/templates/pydantic_form/text.jinja2 +0 -16
- fastlife/templates/pydantic_form/union.jinja2 +0 -40
- fastlife/templates/pydantic_form/widget.jinja2 +0 -10
- fastlife/templating/renderer/jinja2.py +0 -110
- fastlifeweb-0.2.2.dist-info/RECORD +0 -50
- {fastlifeweb-0.2.2.dist-info → fastlifeweb-0.3.0.dist-info}/LICENSE +0 -0
@@ -16,13 +16,14 @@ class DropDownWidget(Widget):
|
|
16
16
|
help_text: Optional[str] = None,
|
17
17
|
) -> None:
|
18
18
|
super().__init__(name, title=title, token=token, removable=removable)
|
19
|
-
self.options =
|
20
|
-
|
21
|
-
if
|
22
|
-
|
23
|
-
|
19
|
+
self.options: list[dict[str, str]] = []
|
20
|
+
for opt in options:
|
21
|
+
if isinstance(opt, tuple):
|
22
|
+
self.options.append({"value": opt[0], "text": opt[1]})
|
23
|
+
else:
|
24
|
+
self.options.append({"value": opt, "text": opt})
|
24
25
|
self.value = value
|
25
26
|
self.help_text = help_text
|
26
27
|
|
27
28
|
def get_template(self) -> str:
|
28
|
-
return "pydantic_form
|
29
|
+
return "pydantic_form.Dropdown"
|
@@ -3,6 +3,7 @@ from collections.abc import MutableSequence, Sequence
|
|
3
3
|
from decimal import Decimal
|
4
4
|
from types import NoneType
|
5
5
|
from typing import Any, Literal, Mapping, Optional, Type, get_origin
|
6
|
+
from uuid import UUID
|
6
7
|
|
7
8
|
from markupsafe import Markup
|
8
9
|
from pydantic import BaseModel, EmailStr, SecretStr, ValidationError
|
@@ -26,16 +27,17 @@ class WidgetFactory:
|
|
26
27
|
self.renderer = renderer
|
27
28
|
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
28
29
|
|
29
|
-
|
30
|
+
def get_markup(
|
30
31
|
self,
|
31
32
|
base: Type[Any],
|
32
33
|
form_data: Mapping[str, Any],
|
33
34
|
*,
|
34
35
|
prefix: str,
|
35
36
|
removable: bool,
|
37
|
+
field: FieldInfo | None = None,
|
36
38
|
) -> Markup:
|
37
|
-
return
|
38
|
-
base, form_data, prefix=prefix, removable=removable
|
39
|
+
return self.get_widget(
|
40
|
+
base, form_data, prefix=prefix, removable=removable, field=field
|
39
41
|
).to_html(self.renderer)
|
40
42
|
|
41
43
|
def get_widget(
|
@@ -45,9 +47,14 @@ class WidgetFactory:
|
|
45
47
|
*,
|
46
48
|
prefix: str,
|
47
49
|
removable: bool,
|
50
|
+
field: FieldInfo | None = None,
|
48
51
|
) -> Widget:
|
49
52
|
return self.build(
|
50
|
-
base,
|
53
|
+
base,
|
54
|
+
value=form_data.get(prefix, {}),
|
55
|
+
name=prefix,
|
56
|
+
removable=removable,
|
57
|
+
field=field,
|
51
58
|
)
|
52
59
|
|
53
60
|
def build(
|
@@ -57,7 +64,7 @@ class WidgetFactory:
|
|
57
64
|
name: str = "",
|
58
65
|
value: Any,
|
59
66
|
removable: bool,
|
60
|
-
field:
|
67
|
+
field: FieldInfo | None = None,
|
61
68
|
) -> Widget:
|
62
69
|
type_origin = get_origin(typ)
|
63
70
|
if type_origin:
|
@@ -77,7 +84,7 @@ class WidgetFactory:
|
|
77
84
|
if issubclass(typ, BaseModel): # if it raises here, the type_origin is unknown
|
78
85
|
return self.build_model(name, typ, field, value or {}, removable)
|
79
86
|
|
80
|
-
if issubclass(typ,
|
87
|
+
if issubclass(typ, bool):
|
81
88
|
return self.build_boolean(name, typ, field, value or False, removable)
|
82
89
|
|
83
90
|
if issubclass(typ, EmailStr):
|
@@ -86,7 +93,7 @@ class WidgetFactory:
|
|
86
93
|
if issubclass(typ, SecretStr):
|
87
94
|
return self.build_secretstr(name, typ, field, value or "", removable)
|
88
95
|
|
89
|
-
if issubclass(typ, (int, str, float, Decimal)):
|
96
|
+
if issubclass(typ, (int, str, float, Decimal, UUID)):
|
90
97
|
return self.build_simpletype(name, typ, field, value or "", removable)
|
91
98
|
|
92
99
|
raise NotImplementedError(f"{typ} not implemented")
|
@@ -21,15 +21,13 @@ class ModelWidget(Widget):
|
|
21
21
|
self.children_widget = children_widget
|
22
22
|
|
23
23
|
def get_template(self) -> str:
|
24
|
-
return "pydantic_form
|
24
|
+
return "pydantic_form.Model"
|
25
25
|
|
26
|
-
|
26
|
+
def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
|
27
27
|
"""Return the html version"""
|
28
|
-
children_widget = [
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
)
|
35
|
-
)
|
28
|
+
children_widget = [child.to_html(renderer) for child in self.children_widget]
|
29
|
+
kwargs = {
|
30
|
+
"widget": self,
|
31
|
+
"children_widget": children_widget,
|
32
|
+
}
|
33
|
+
return Markup(renderer.render_template(self.get_template(), **kwargs))
|
@@ -27,16 +27,16 @@ class SequenceWidget(Widget):
|
|
27
27
|
self.help_text = help_text
|
28
28
|
|
29
29
|
def get_template(self) -> str:
|
30
|
-
return "pydantic_form/
|
30
|
+
return "pydantic_form/Sequence"
|
31
31
|
|
32
32
|
def build_item_type(self, route_prefix: str) -> TypeWrapper:
|
33
33
|
return TypeWrapper(self.item_type, route_prefix, self.name, self.token)
|
34
34
|
|
35
|
-
|
35
|
+
def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
|
36
36
|
"""Return the html version"""
|
37
|
-
children = [Markup(
|
37
|
+
children = [Markup(item.to_html(renderer)) for item in self.items]
|
38
38
|
return Markup(
|
39
|
-
|
39
|
+
renderer.render_template(
|
40
40
|
self.get_template(),
|
41
41
|
widget=self,
|
42
42
|
type=self.build_item_type(renderer.route_prefix),
|
@@ -31,13 +31,13 @@ class UnionWidget(Widget):
|
|
31
31
|
]
|
32
32
|
|
33
33
|
def get_template(self) -> str:
|
34
|
-
return "pydantic_form
|
34
|
+
return "pydantic_form.Union"
|
35
35
|
|
36
|
-
|
36
|
+
def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
|
37
37
|
"""Return the html version"""
|
38
|
-
child = Markup(
|
38
|
+
child = Markup(self.child.to_html(renderer)) if self.child else ""
|
39
39
|
return Markup(
|
40
|
-
|
40
|
+
renderer.render_template(
|
41
41
|
self.get_template(),
|
42
42
|
widget=self,
|
43
43
|
types=self.build_types(renderer.route_prefix),
|
@@ -46,6 +46,7 @@ class UnionWidget(Widget):
|
|
46
46
|
renderer.route_prefix,
|
47
47
|
self.parent_name,
|
48
48
|
self.token,
|
49
|
+
title=self.title,
|
49
50
|
),
|
50
51
|
child=child,
|
51
52
|
)
|
fastlife/testing/testclient.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import re
|
2
2
|
import time
|
3
3
|
from collections.abc import MutableMapping
|
4
|
-
from typing import Any, Literal, Mapping
|
4
|
+
from typing import Any, Iterator, Literal, Mapping, Optional, Sequence
|
5
5
|
from urllib.parse import urlencode
|
6
6
|
|
7
7
|
import bs4
|
@@ -22,15 +22,51 @@ class Element:
|
|
22
22
|
def click(self) -> "WebResponse":
|
23
23
|
return self._client.get(self._tag.attrs["href"])
|
24
24
|
|
25
|
+
@property
|
26
|
+
def node_name(self) -> str:
|
27
|
+
return self._tag.name
|
28
|
+
|
25
29
|
@property
|
26
30
|
def attrs(self) -> dict[str, str]:
|
27
31
|
return self._tag.attrs
|
28
32
|
|
33
|
+
@property
|
34
|
+
def text(self) -> str:
|
35
|
+
return self._tag.text.strip()
|
36
|
+
|
29
37
|
@property
|
30
38
|
def form(self) -> "Element | None":
|
31
39
|
return Element(self._client, self._tag.form) if self._tag.form else None
|
32
40
|
|
41
|
+
@property
|
42
|
+
def h1(self) -> "Element":
|
43
|
+
nodes = self.by_node_name("h1")
|
44
|
+
assert len(nodes) == 1, f"Should have 1 <h1>, got {len(nodes)} in {self}"
|
45
|
+
return nodes[0]
|
46
|
+
|
47
|
+
@property
|
48
|
+
def h2(self) -> Sequence["Element"]:
|
49
|
+
return self.by_node_name("h2")
|
50
|
+
|
51
|
+
@property
|
52
|
+
def hx_target(self) -> Optional[str]:
|
53
|
+
el: bs4.Tag | None = self._tag
|
54
|
+
while el:
|
55
|
+
if "hx-target" in el.attrs:
|
56
|
+
ret = el.attrs["hx-target"]
|
57
|
+
if ret == "this":
|
58
|
+
ret = el.attrs["id"]
|
59
|
+
return ret
|
60
|
+
el = el.parent
|
61
|
+
return None
|
62
|
+
|
33
63
|
def by_text(self, text: str, *, node_name: str | None = None) -> "Element | None":
|
64
|
+
nodes = self.iter_all_by_text(text, node_name=node_name)
|
65
|
+
return next(nodes, None)
|
66
|
+
|
67
|
+
def iter_all_by_text(
|
68
|
+
self, text: str, *, node_name: str | None = None
|
69
|
+
) -> "Iterator[Element]":
|
34
70
|
nodes = self._tag.find_all(string=re.compile(rf"\s*{text}\s*"))
|
35
71
|
for node in nodes:
|
36
72
|
if isinstance(node, bs4.NavigableString):
|
@@ -39,12 +75,18 @@ class Element:
|
|
39
75
|
if node_name:
|
40
76
|
while node is not None:
|
41
77
|
if node.name == node_name:
|
42
|
-
|
78
|
+
yield Element(self._client, node)
|
43
79
|
node = node.parent
|
44
80
|
elif node:
|
45
|
-
|
81
|
+
yield Element(self._client, node)
|
46
82
|
return None
|
47
83
|
|
84
|
+
def get_all_by_text(
|
85
|
+
self, text: str, *, node_name: str | None = None
|
86
|
+
) -> "Sequence[Element]":
|
87
|
+
nodes = self.iter_all_by_text(text, node_name=node_name)
|
88
|
+
return list(nodes)
|
89
|
+
|
48
90
|
def by_label_text(self, text: str) -> "Element | None":
|
49
91
|
label = self.by_text(text, node_name="label")
|
50
92
|
assert label is not None
|
@@ -60,37 +102,84 @@ class Element:
|
|
60
102
|
Element(self._client, e) for e in self._tag.find_all(node_name, attrs or {})
|
61
103
|
]
|
62
104
|
|
105
|
+
def __repr__(self) -> str:
|
106
|
+
return f"<{self.node_name}>"
|
107
|
+
|
108
|
+
def __str__(self) -> str:
|
109
|
+
return str(self._tag)
|
110
|
+
|
63
111
|
|
64
112
|
class WebForm:
|
65
113
|
def __init__(self, client: "WebTestClient", origin: str, form: Element):
|
66
114
|
self._client = client
|
67
115
|
self._form = form
|
68
116
|
self._origin = origin
|
117
|
+
self._formfields: dict[str, Element] = {}
|
69
118
|
self._formdata: dict[str, str] = {}
|
70
119
|
inputs = self._form.by_node_name("input")
|
71
120
|
for input in inputs:
|
121
|
+
self._formfields[input.attrs["name"]] = input
|
72
122
|
if input.attrs.get("type") == "checkbox" and "checked" not in input.attrs:
|
73
123
|
continue
|
74
124
|
self._formdata[input.attrs["name"]] = input.attrs.get("value", "")
|
75
|
-
|
125
|
+
|
126
|
+
inputs = self._form.by_node_name("select")
|
127
|
+
for input in inputs:
|
128
|
+
self._formfields[input.attrs["name"]] = input
|
129
|
+
for option in input.by_node_name("options"):
|
130
|
+
if "selected" in option.attrs:
|
131
|
+
self._formdata[input.attrs["name"]] = option.attrs.get(
|
132
|
+
"value", option.text
|
133
|
+
)
|
134
|
+
# field textearea...
|
76
135
|
|
77
136
|
def set(self, fieldname: str, value: str) -> Any:
|
78
|
-
if fieldname not in self.
|
137
|
+
if fieldname not in self._formfields:
|
79
138
|
raise ValueError(f"{fieldname} does not exists")
|
139
|
+
if self._formfields[fieldname].node_name == "select":
|
140
|
+
raise RuntimeError(f"{fieldname} is a <select>, use select() instead")
|
80
141
|
self._formdata[fieldname] = value
|
81
142
|
|
82
|
-
def
|
83
|
-
|
143
|
+
def select(self, fieldname: str, value: str) -> Any:
|
144
|
+
if fieldname not in self._formfields:
|
145
|
+
raise ValueError(f"{fieldname} does not exists")
|
146
|
+
if self._formfields[fieldname].node_name != "select":
|
147
|
+
raise RuntimeError(
|
148
|
+
f"{fieldname} is a <{self._formfields[fieldname]}>, use set() instead"
|
149
|
+
)
|
150
|
+
if "multiple" in self._formfields[fieldname].attrs:
|
151
|
+
raise NotImplementedError
|
152
|
+
for option in self._formfields[fieldname].by_node_name("option"):
|
153
|
+
if option.text == value.strip():
|
154
|
+
self._formdata[fieldname] = option.attrs.get("value", option.text)
|
155
|
+
break
|
156
|
+
else:
|
157
|
+
raise ValueError(f'No option {value} in <select name="{fieldname}">')
|
158
|
+
|
159
|
+
def button(self, text: str, position: int = 0) -> "WebForm":
|
160
|
+
buttons = self._form.get_all_by_text(text, node_name="button")
|
161
|
+
assert len(buttons) > position, f'Button "{text}" not found'
|
162
|
+
button = buttons[position]
|
163
|
+
if "name" in button.attrs:
|
164
|
+
self._formdata[button.attrs["name"]] = button.attrs.get("value", "")
|
84
165
|
return self
|
85
166
|
|
86
167
|
def submit(self, follow_redirects: bool = True) -> "WebResponse":
|
168
|
+
headers = {}
|
87
169
|
target = (
|
88
170
|
self._form.attrs.get("hx-post")
|
89
171
|
or self._form.attrs.get("post")
|
90
172
|
or self._origin
|
91
173
|
)
|
174
|
+
if "hx-post" in self._form.attrs:
|
175
|
+
if hx_target := self._form.hx_target:
|
176
|
+
headers["HX-Target"] = hx_target
|
177
|
+
|
92
178
|
return self._client.post(
|
93
|
-
target,
|
179
|
+
target,
|
180
|
+
data=self._formdata,
|
181
|
+
headers=headers,
|
182
|
+
follow_redirects=follow_redirects,
|
94
183
|
)
|
95
184
|
|
96
185
|
def __contains__(self, key: str) -> bool:
|
@@ -293,6 +382,15 @@ class WebTestClient:
|
|
293
382
|
headers=headers,
|
294
383
|
max_redirects=max_redirects - 1,
|
295
384
|
)
|
385
|
+
if "HX-Redirect" in resp.headers and max_redirects > 0:
|
386
|
+
return self.request(
|
387
|
+
method="GET",
|
388
|
+
url=resp.headers["HX-Redirect"],
|
389
|
+
content=None,
|
390
|
+
headers=headers,
|
391
|
+
max_redirects=max_redirects - 1,
|
392
|
+
)
|
393
|
+
|
296
394
|
return resp
|
297
395
|
|
298
396
|
def get(self, url: str, follow_redirects: bool = True) -> WebResponse:
|
@@ -303,12 +401,19 @@ class WebTestClient:
|
|
303
401
|
)
|
304
402
|
|
305
403
|
def post(
|
306
|
-
self,
|
404
|
+
self,
|
405
|
+
url: str,
|
406
|
+
data: Mapping[str, Any],
|
407
|
+
*,
|
408
|
+
headers: Mapping[str, Any] | None = None,
|
409
|
+
follow_redirects: bool = True,
|
307
410
|
) -> WebResponse:
|
411
|
+
if headers is None:
|
412
|
+
headers = {}
|
308
413
|
return self.request(
|
309
414
|
"POST",
|
310
415
|
url,
|
311
416
|
content=urlencode(data),
|
312
|
-
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
417
|
+
headers={"Content-Type": "application/x-www-form-urlencoded", **headers},
|
313
418
|
max_redirects=int(follow_redirects) * 10,
|
314
419
|
)
|
fastlife/views/pydantic_form.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import Optional
|
2
2
|
|
3
3
|
from fastapi import Query, Response
|
4
|
+
from pydantic.fields import FieldInfo
|
4
5
|
|
5
6
|
from fastlife import Configurator, configure
|
6
7
|
from fastlife.configurator.registry import Registry
|
@@ -10,12 +11,16 @@ from fastlife.shared_utils.resolver import resolve_extended
|
|
10
11
|
async def show_widget(
|
11
12
|
typ: str,
|
12
13
|
reg: Registry,
|
13
|
-
|
14
|
-
|
14
|
+
title: Optional[str] = Query(None),
|
15
|
+
name: Optional[str] = Query(None),
|
16
|
+
token: Optional[str] = Query(None),
|
15
17
|
removable: bool = Query(False),
|
16
18
|
) -> Response:
|
17
19
|
model_cls = resolve_extended(typ)
|
18
|
-
|
20
|
+
field = None
|
21
|
+
if title:
|
22
|
+
field = FieldInfo(title=title)
|
23
|
+
data = reg.renderer.pydantic_form(model_cls, None, name, token, removable, field)
|
19
24
|
return Response(data, headers={"Content-Type": "text/html"})
|
20
25
|
|
21
26
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fastlifeweb
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: High-level web framework
|
5
5
|
License: BSD-derived
|
6
6
|
Author: Guillaume Gauvrit
|
@@ -20,7 +20,7 @@ Requires-Dist: beautifulsoup4[testing] (>=4.12.2,<5.0.0)
|
|
20
20
|
Requires-Dist: behave (>=1.2.6,<2.0.0)
|
21
21
|
Requires-Dist: fastapi (>=0.108.0,<0.109.0)
|
22
22
|
Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
|
23
|
-
Requires-Dist:
|
23
|
+
Requires-Dist: jinjax (>=0.31,<0.32)
|
24
24
|
Requires-Dist: markupsafe (>=2.1.3,<3.0.0)
|
25
25
|
Requires-Dist: pydantic (>=2.3.0,<3.0.0)
|
26
26
|
Requires-Dist: pydantic-settings (>=2.0.3,<3.0.0)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
fastlife/__init__.py,sha256=ewfV5kR3Pq7Bzan_Wnq__LYGzXgk2CJaamxhyMyhaCI,312
|
2
|
+
fastlife/configurator/__init__.py,sha256=2EPjM1o5iHJIViPwgJjaPQS3pMhE-9dik_mm53eX2DY,91
|
3
|
+
fastlife/configurator/base.py,sha256=2ahvTudLmD99YQjnIeGN5JDPCSl3k-mauu7bsSEB5RE,216
|
4
|
+
fastlife/configurator/configurator.py,sha256=6BaB7SR24Q4Qvs8NrCpatRkkZiPXf9mKLID6RxOKxDg,5740
|
5
|
+
fastlife/configurator/registry.py,sha256=VBwKWPZROzE0aQgQdqVjurLAwkh9VxpwvaKzain6Nxk,1449
|
6
|
+
fastlife/configurator/settings.py,sha256=RrTE-T7CieXwg-WI5Q1eGZA8PbawJLmLxX7puX4dHbQ,1387
|
7
|
+
fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
fastlife/request/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
fastlife/request/form_data.py,sha256=gKXo92ZwsMX6UpMkf4BlNYDiOfwxyzDzItc_lpplAzY,3642
|
10
|
+
fastlife/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
fastlife/security/csrf.py,sha256=47epJVJtr5X6j-Hog54WCGOoiRLQQHvgBU71iqR1N0A,1025
|
12
|
+
fastlife/security/policy.py,sha256=5jV5nypy5O8XPFFRJ_bzG8Ltk5xcPWclkz23qiG1_I8,509
|
13
|
+
fastlife/session/__init__.py,sha256=OnzRCYRzc1lw9JB0UdKi-aRLPNT2n8mM8kwY1P4w7uU,838
|
14
|
+
fastlife/session/middleware.py,sha256=JgXdBlxlm9zIEgXcidbBrMAp5wJVPsZWtvCLVDk5h2s,3049
|
15
|
+
fastlife/session/serializer.py,sha256=qpVnHQjYTxw3aOnoEOKIjOFJg2z45KjiX5sipWk2gws,1458
|
16
|
+
fastlife/shared_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
fastlife/shared_utils/infer.py,sha256=_hmGzu84VlZAkdw_owkW8eHknULqH3MLDBlXj7LkEsc,466
|
18
|
+
fastlife/shared_utils/resolver.py,sha256=wXQQTB4jf86m4qENhMOkHkWpLJj_T4-_eND_ItTLnTE,1410
|
19
|
+
fastlife/templates/A.jinja,sha256=sQov2Hze4NfpmQ3FOYHeXugOb_SZw6ncvDZNyCxy0HI,481
|
20
|
+
fastlife/templates/Button.jinja,sha256=KK3AztRIvBXZh2jUXMrBiz80XEt9s9STfBiVKR_1FvA,1064
|
21
|
+
fastlife/templates/Checkbox.jinja,sha256=SZJYgijSr9rGBSLZC4DXyHpoFNLnC1QyvgpUUkbi9xE,98
|
22
|
+
fastlife/templates/CsrfToken.jinja,sha256=CZrkuKJNIpcsZ3yBvQ4csUcBSHu2xJ13q11DI5VAgcI,80
|
23
|
+
fastlife/templates/Form.jinja,sha256=Z-YrJioBlxSkh-xKDcXmBA7sV2vkzSzMqSjQFEe5AGU,147
|
24
|
+
fastlife/templates/H1.jinja,sha256=jQb4mtR_kipxrwN7cH6Ha9otNP9r1SJhPJF6RiXu-Bw,151
|
25
|
+
fastlife/templates/H2.jinja,sha256=WGyjUhuh2UL-iGGhHLwCeHTMGMEEewZ1683VZxIhMfY,151
|
26
|
+
fastlife/templates/Hidden.jinja,sha256=nOGfkWwhXckJE2O4wL5IXNKB5dvU0HbmdWCTECxEFHs,120
|
27
|
+
fastlife/templates/Input.jinja,sha256=2GQ-7a9woZaz3JRghe-E09p5sP7q1B3wU9ZWsPVee54,612
|
28
|
+
fastlife/templates/Label.jinja,sha256=QVw9R9Wf5vV51dsdLaJUxv6hV2AiURdkGSRJ2-RgZ9Y,147
|
29
|
+
fastlife/templates/Option.jinja,sha256=zxY0l5cBgyv37fEo5jldtYvQoQNNIvtzQV1MLu-4reU,65
|
30
|
+
fastlife/templates/Radio.jinja,sha256=RNqN2RkekqOXIne9b1PhuOV_fZSCWD2Ws8fYeBvBucQ,654
|
31
|
+
fastlife/templates/Select.jinja,sha256=DVheZlrxrcQGClyE5g44V9JInkPcV13v4cUGHNEbiWs,407
|
32
|
+
fastlife/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
|
+
fastlife/templates/pydantic_form/Boolean.jinja,sha256=OLgifXvyqDB2T3w8V6oNZCa1DO5G6WgNe-0jB8lQAE0,279
|
34
|
+
fastlife/templates/pydantic_form/Dropdown.jinja,sha256=DKP_vcC89W4PLQL2QwzUiPiDx94gBre5fYejRn_qtGM,539
|
35
|
+
fastlife/templates/pydantic_form/Hidden.jinja,sha256=n6CbTSwZr2E_oY8TO2WPbnrLHBaWfe_CXVCYOYmCfts,83
|
36
|
+
fastlife/templates/pydantic_form/Model.jinja,sha256=tvaK6rnGzonBsRtyi-jxtxRVRpyC3tM0FgIS5ZLjIQo,493
|
37
|
+
fastlife/templates/pydantic_form/Sequence.jinja,sha256=2HDlTmmsd87WEIn3WSYOrC4MePHAybgS_64j8cbJVrI,1409
|
38
|
+
fastlife/templates/pydantic_form/Text.jinja,sha256=TrxKR-xLeBC8Bw1KyBj22YaJysLhdF-6N6LeT_oZous,503
|
39
|
+
fastlife/templates/pydantic_form/Union.jinja,sha256=W5r7KHIL2A_2Vc8Z4xtwTQzeVBEkY1wVqzMMLvmvG60,1003
|
40
|
+
fastlife/templates/pydantic_form/Widget.jinja,sha256=8raoMjtO4ARBfbz8EG-HKT112KkrWG82BbUfbXpAmZs,287
|
41
|
+
fastlife/templating/__init__.py,sha256=RKqAsVyy0mS5qiujpmRbbe-FL1exf46Zo71_0QJwmrA,220
|
42
|
+
fastlife/templating/binding.py,sha256=XlABwvd1UFt6ymbcqLOR_MXz6HlXkwpdlIiKhAgD5F0,1907
|
43
|
+
fastlife/templating/renderer/__init__.py,sha256=ndygjyodU5F8EVzBLVvXq5fLpnPo5lTyk6yia79RHJY,156
|
44
|
+
fastlife/templating/renderer/abstract.py,sha256=qVQ3d7gyesKx6Pz25XN0-NuI2088oKvkOUjGHiT1ALk,801
|
45
|
+
fastlife/templating/renderer/jinjax.py,sha256=dfdaq3KuZXA1Jnmny_cIzcVzLXvr9ESqcgQIrLRlaQ0,2379
|
46
|
+
fastlife/templating/renderer/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
|
+
fastlife/templating/renderer/widgets/base.py,sha256=EnztAzopgEJMQMNf3xyOlf9P3i4pZzq15uSK5bf9wfg,2446
|
48
|
+
fastlife/templating/renderer/widgets/boolean.py,sha256=RVp2jLeVR3f6JHl-9yBDZHcr-p2qufw0NVm12D_62aU,454
|
49
|
+
fastlife/templating/renderer/widgets/dropdown.py,sha256=TuNxww-2yqTM3pdqG_gMYD3r9BWVxbhniFBx2l7Gfg8,891
|
50
|
+
fastlife/templating/renderer/widgets/factory.py,sha256=oz_qKiMgTmPmE8mQSXY8aLgPc6u4tUArTJtJ2y66vbo,9626
|
51
|
+
fastlife/templating/renderer/widgets/hidden.py,sha256=PP_mOR2cd9LQURHDrfJ3b85pR3F1SEzsv7n_UhUq-dA,317
|
52
|
+
fastlife/templating/renderer/widgets/model.py,sha256=t0MatCCA6pUsilcQ3ZTyK9a2LtWTbFzdrBFyQ3sqv6k,943
|
53
|
+
fastlife/templating/renderer/widgets/sequence.py,sha256=7nHl8je6oWfwn0jbHLau_S_vF-pbB_Alzjtwv5I4ejg,1355
|
54
|
+
fastlife/templating/renderer/widgets/text.py,sha256=_bmr6FMMFW_oZZdZa_4TH3DfcBpAjOdzm2a5yoonW7o,782
|
55
|
+
fastlife/templating/renderer/widgets/union.py,sha256=VKfjikIujNATxEslPTVydwzA8EOu_l_sPlOgK_cGcrc,1658
|
56
|
+
fastlife/testing/__init__.py,sha256=KgTlRI0g8z7HRpL7mD5QgI__LT9Y4QDSzKMlxJG3wNk,67
|
57
|
+
fastlife/testing/testclient.py,sha256=7aYCHSd0EjsmN4zMboDEh3-xcAZdw7LTvtovaDGZGrg,13780
|
58
|
+
fastlife/views/__init__.py,sha256=nn4B_8YTbTmhGPvSd20yyKK_9Dh1Pfh_Iq7z6iK8-CE,154
|
59
|
+
fastlife/views/pydantic_form.py,sha256=5UymAhbhxsNbs_kjSKI5DEk_jeiCAgtbJofYG_ukcSA,1038
|
60
|
+
fastlifeweb-0.3.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
|
61
|
+
fastlifeweb-0.3.0.dist-info/METADATA,sha256=uPOHHVijDGXd5tDNy8EFo-ztIYMKYtXh7gVPv3ShTmQ,1748
|
62
|
+
fastlifeweb-0.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
63
|
+
fastlifeweb-0.3.0.dist-info/RECORD,,
|
fastlife/templates/base.jinja2
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
{# all the macros here are globals, available in all templates #}
|
2
|
-
|
3
|
-
|
4
|
-
{% macro title(title="") %}
|
5
|
-
<h2
|
6
|
-
class="block pb-4 font-sans text-xl font-bold leading-tight tracking-tight text-neutral-900 md:text-4xl dark:text-white">
|
7
|
-
{{- title -}}
|
8
|
-
{%- if caller %}{{ caller() }}{% endif -%}
|
9
|
-
</h2>
|
10
|
-
{% endmacro %}
|
11
|
-
|
12
|
-
{% macro link(label, href, target="#maincontent", swap="innerHTML show:body:top", push_url=true) %}
|
13
|
-
<a href="{{href}}" hx-get="{{href}}" hx-target="{{target}}" hx-swap="{{swap}}" {% if push_url %}hx-push-url="true" {%-
|
14
|
-
endif %}
|
15
|
-
class="cursor-pointer px-4 py-2 text-sm font-medium text-neutral-900 bg-white border-t border-b border-neutral-200 hover:bg-neutral-100 hover:text-primary-700 focus:z-10 focus:ring-2 focus:ring-primary-700 focus:text-primary-700 dark:bg-neutral-700 dark:border-neutral-600 dark:text-white dark:hover:text-white dark:hover:bg-neutral-600 dark:focus:ring-primary-500 dark:focus:text-white">
|
16
|
-
{{- label -}}
|
17
|
-
{%- if caller %}{{ caller() }}{% endif -%}
|
18
|
-
</a>
|
19
|
-
{% endmacro %}
|
20
|
-
|
21
|
-
{% macro button(
|
22
|
-
title,
|
23
|
-
type="submit",
|
24
|
-
id="",
|
25
|
-
name="action",
|
26
|
-
value="submit",
|
27
|
-
onclick="",
|
28
|
-
target="",
|
29
|
-
swap="",
|
30
|
-
get="",
|
31
|
-
select="",
|
32
|
-
after_request="",
|
33
|
-
vals="",
|
34
|
-
full_width=false,
|
35
|
-
hidden=false,
|
36
|
-
aria_label=""
|
37
|
-
) %}
|
38
|
-
<button type="{{type}}" {%if id %}id="{{id}}" {%endif%}name="{{name}}" value="{{value}}" {% if target
|
39
|
-
%}hx-target="{{target}}" {% endif %} {% if swap %}hx-swap="{{swap}}" {% endif %} {% if select %}hx-select="{{select}}"
|
40
|
-
{% endif %} {% if get %}hx-get="{{get}}" {% endif %} {% if onclick %}onclick="{{onclick}}" {% endif %}{% if
|
41
|
-
after_request %}hx-on::after-request="{{after_request}}" {% endif %} {% if vals %}hx-vals='{{vals|safe}}' {% endif %}
|
42
|
-
{% if aria_label %}aria-label="{{aria_label}}" {% endif %}
|
43
|
-
class="{% if full_width %}w-full {% endif %}text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
44
|
-
{% if hidden %}hidden{% endif %}>
|
45
|
-
{{- title -}}
|
46
|
-
</button>
|
47
|
-
{% endmacro %}
|
48
|
-
|
49
|
-
{% macro input(label, name, id, aria_label="", placeholder="", value="", type="text", required=false) %}
|
50
|
-
<div>
|
51
|
-
<label for="{{id}}" class="block mb-2 text-sm font-medium text-neutral-900 dark:text-white">
|
52
|
-
{{label}}
|
53
|
-
</label>
|
54
|
-
<input type="{{type}}" name="{{name}}" id="{{id}}" value="{{value}}" {%if placeholder %}placeholder="{{placeholder}}"
|
55
|
-
{%endif%} {%if required%}required="" {%endif%} {% if aria_label %}aria-label="{{aria_label}}" {% endif %} class="bg-neutral-50 border border-neutral-300 text-neutral-900 sm:text-sm rounded-lg focus:ring-primary-600
|
56
|
-
focus:border-primary-600 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400
|
57
|
-
dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
58
|
-
</div>
|
59
|
-
{% endmacro %}
|
60
|
-
|
61
|
-
{% macro checkbox(label, name, id, placeholder, checked=false) %}
|
62
|
-
<div class="flex items-center mb-4">
|
63
|
-
<input type="checkbox" name="{{name}}" id="{{id}}" {% if checked %}checked="" {% endif %}
|
64
|
-
class="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-neutral-800 focus:ring-2 dark:bg-neutral-700 dark:border-neutral-600"
|
65
|
-
placeholder="{{placeholder}}" {%if required%}required="" {%endif%}>
|
66
|
-
<label for="{{id}}" class="block ml-2 text-sm font-medium text-neutral-900 dark:text-white">
|
67
|
-
{{label}}
|
68
|
-
</label>
|
69
|
-
</div>
|
70
|
-
{% endmacro %}
|
71
|
-
|
72
|
-
{% macro hidden(name, value) %}
|
73
|
-
<div>
|
74
|
-
<input type="hidden" name="{{name}}" value="{{value}}">
|
75
|
-
</div>
|
76
|
-
{% endmacro %}
|
77
|
-
|
78
|
-
{% macro form() %}
|
79
|
-
<form class="space-y-4 md:space-y-6" hx-post="" method="post">
|
80
|
-
{{ hidden(get_csrf_token_name(), get_csrf_token()) }}
|
81
|
-
{{ caller() }}
|
82
|
-
</form>
|
83
|
-
{% endmacro %}
|
@@ -1,8 +0,0 @@
|
|
1
|
-
{% from "globals.jinja2" import button %}
|
2
|
-
{% from "pydantic_form/widget.jinja2" import show_widget %}
|
3
|
-
{% call show_widget(widget) %}
|
4
|
-
<div class="pt-4">
|
5
|
-
<label for="{{widget.id}}">{{widget.title}}</label>
|
6
|
-
<input name="{{widget.name}}" value="{{widget.value}}" type="checkbox" id="{{widget.id}}" />
|
7
|
-
</div>
|
8
|
-
{% endcall %}
|
@@ -1,18 +0,0 @@
|
|
1
|
-
{% from "globals.jinja2" import button %}
|
2
|
-
{% from "pydantic_form/widget.jinja2" import show_widget %}
|
3
|
-
{% call show_widget(widget) %}
|
4
|
-
<div class="pt-4">
|
5
|
-
<label for="{{widget.id}}" class="block mb-2 text-base font-bold text-neutral-900 dark:text-white">
|
6
|
-
{{ widget.title }}
|
7
|
-
</label>
|
8
|
-
<select name="{{widget.name}}" id="{{widget.id}}"
|
9
|
-
class="bg-neutral-50 border border-neutral-300 text-neutral-900 text-base rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-neutral-700 dark:border-neutral-600 dark:placeholder-neutral-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
10
|
-
{%- for opt in widget.options %}
|
11
|
-
<option value="{{opt[0]}}" {%- if opt[0] == widget.value %} selected{% endif %}>{{ opt[1] }}</option>
|
12
|
-
{%- endfor -%}
|
13
|
-
</select>
|
14
|
-
{%- if widget.help_text -%}
|
15
|
-
<span class="mt-2 text-sm text-neutral-500 dark:text-neutral-400">{{widget.help_text}}</span>
|
16
|
-
{%- endif %}
|
17
|
-
</div>
|
18
|
-
{% endcall %}
|
@@ -1 +0,0 @@
|
|
1
|
-
<input name="{{widget.name}}" value="{{widget.value}}" type="hidden" id="{{widget.id}}" />
|
@@ -1,16 +0,0 @@
|
|
1
|
-
{% from "globals.jinja2" import button %}
|
2
|
-
{% from "pydantic_form/widget.jinja2" import show_widget %}
|
3
|
-
{% call show_widget(widget) %}
|
4
|
-
<div id="{{widget.id}}" class="m-4">
|
5
|
-
<details open>
|
6
|
-
<summary class="justify-between items-center font-medium cursor-pointer">
|
7
|
-
<h4 class="inline font-sans text-3xl font-bold">{{widget.title}}</h4>
|
8
|
-
</summary>
|
9
|
-
<div>
|
10
|
-
{% for child in children_widget %}
|
11
|
-
{{ child }}
|
12
|
-
{% endfor %}
|
13
|
-
</div>
|
14
|
-
</details>
|
15
|
-
</div>
|
16
|
-
{% endcall %}
|