fastlifeweb 0.2.3__py3-none-any.whl → 0.3.1__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.
Files changed (63) hide show
  1. fastlife/__init__.py +5 -0
  2. fastlife/configurator/configurator.py +2 -2
  3. fastlife/configurator/registry.py +7 -3
  4. fastlife/configurator/settings.py +1 -1
  5. fastlife/request/form_data.py +2 -7
  6. fastlife/security/csrf.py +1 -1
  7. fastlife/session/middleware.py +0 -1
  8. fastlife/shared_utils/__init__.py +0 -0
  9. fastlife/shared_utils/resolver.py +5 -3
  10. fastlife/templates/A.jinja +5 -0
  11. fastlife/templates/Button.jinja +27 -0
  12. fastlife/templates/Checkbox.jinja +2 -0
  13. fastlife/templates/CsrfToken.jinja +2 -0
  14. fastlife/templates/Form.jinja +5 -0
  15. fastlife/templates/H1.jinja +4 -0
  16. fastlife/templates/H2.jinja +4 -0
  17. fastlife/templates/Hidden.jinja +6 -0
  18. fastlife/templates/Input.jinja +8 -0
  19. fastlife/templates/Label.jinja +3 -0
  20. fastlife/templates/Option.jinja +2 -0
  21. fastlife/templates/Radio.jinja +8 -0
  22. fastlife/templates/Select.jinja +8 -0
  23. fastlife/templates/__init__.py +0 -0
  24. fastlife/templates/pydantic_form/Boolean.jinja +7 -0
  25. fastlife/templates/pydantic_form/Dropdown.jinja +17 -0
  26. fastlife/templates/pydantic_form/Hidden.jinja +2 -0
  27. fastlife/templates/pydantic_form/Model.jinja +16 -0
  28. fastlife/templates/pydantic_form/Sequence.jinja +37 -0
  29. fastlife/templates/pydantic_form/Text.jinja +12 -0
  30. fastlife/templates/pydantic_form/Union.jinja +26 -0
  31. fastlife/templates/pydantic_form/Widget.jinja +10 -0
  32. fastlife/templating/__init__.py +2 -2
  33. fastlife/templating/binding.py +6 -7
  34. fastlife/templating/renderer/__init__.py +2 -2
  35. fastlife/templating/renderer/abstract.py +9 -7
  36. fastlife/templating/renderer/jinjax.py +73 -0
  37. fastlife/templating/renderer/widgets/base.py +14 -13
  38. fastlife/templating/renderer/widgets/boolean.py +1 -1
  39. fastlife/templating/renderer/widgets/dropdown.py +7 -6
  40. fastlife/templating/renderer/widgets/factory.py +14 -7
  41. fastlife/templating/renderer/widgets/hidden.py +1 -1
  42. fastlife/templating/renderer/widgets/model.py +8 -10
  43. fastlife/templating/renderer/widgets/sequence.py +4 -4
  44. fastlife/templating/renderer/widgets/text.py +1 -1
  45. fastlife/templating/renderer/widgets/union.py +5 -4
  46. fastlife/testing/testclient.py +115 -10
  47. fastlife/views/pydantic_form.py +8 -3
  48. {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/METADATA +3 -3
  49. fastlifeweb-0.3.1.dist-info/RECORD +63 -0
  50. {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.dist-info}/WHEEL +1 -1
  51. fastlife/templates/base.jinja2 +0 -2
  52. fastlife/templates/globals.jinja2 +0 -83
  53. fastlife/templates/pydantic_form/boolean.jinja2 +0 -8
  54. fastlife/templates/pydantic_form/dropdown.jinja2 +0 -18
  55. fastlife/templates/pydantic_form/hidden.jinja2 +0 -1
  56. fastlife/templates/pydantic_form/model.jinja2 +0 -16
  57. fastlife/templates/pydantic_form/sequence.jinja2 +0 -41
  58. fastlife/templates/pydantic_form/text.jinja2 +0 -16
  59. fastlife/templates/pydantic_form/union.jinja2 +0 -40
  60. fastlife/templates/pydantic_form/widget.jinja2 +0 -10
  61. fastlife/templating/renderer/jinja2.py +0 -110
  62. fastlifeweb-0.2.3.dist-info/RECORD +0 -50
  63. {fastlifeweb-0.2.3.dist-info → fastlifeweb-0.3.1.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
- list(zip(options, options))
21
- if options and not isinstance(options[0], tuple)
22
- else options
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/dropdown.jinja2"
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
- async def get_markup(
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 await self.get_widget(
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, value=form_data.get(prefix, {}), name=prefix, removable=removable
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: Optional[FieldInfo] = None,
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, (bool)):
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")
@@ -13,4 +13,4 @@ class HiddenWidget(Widget):
13
13
  self.value = value
14
14
 
15
15
  def get_template(self) -> str:
16
- return "pydantic_form/hidden.jinja2"
16
+ return "pydantic_form.Hidden"
@@ -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/model.jinja2"
24
+ return "pydantic_form.Model"
25
25
 
26
- async def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
26
+ def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
27
27
  """Return the html version"""
28
- children_widget = [
29
- await child.to_html(renderer) for child in self.children_widget
30
- ]
31
- return Markup(
32
- await renderer.render_template(
33
- self.get_template(), widget=self, children_widget=children_widget
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/sequence.jinja2"
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
- async def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
35
+ def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
36
36
  """Return the html version"""
37
- children = [Markup(await item.to_html(renderer)) for item in self.items]
37
+ children = [Markup(item.to_html(renderer)) for item in self.items]
38
38
  return Markup(
39
- await renderer.render_template(
39
+ renderer.render_template(
40
40
  self.get_template(),
41
41
  widget=self,
42
42
  type=self.build_item_type(renderer.route_prefix),
@@ -26,4 +26,4 @@ class TextWidget(Widget):
26
26
  self.input_type = input_type
27
27
 
28
28
  def get_template(self) -> str:
29
- return "pydantic_form/text.jinja2"
29
+ return "pydantic_form.Text"
@@ -31,13 +31,13 @@ class UnionWidget(Widget):
31
31
  ]
32
32
 
33
33
  def get_template(self) -> str:
34
- return "pydantic_form/union.jinja2"
34
+ return "pydantic_form.Union"
35
35
 
36
- async def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
36
+ def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
37
37
  """Return the html version"""
38
- child = Markup(await self.child.to_html(renderer)) if self.child else ""
38
+ child = Markup(self.child.to_html(renderer)) if self.child else ""
39
39
  return Markup(
40
- await renderer.render_template(
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
  )
@@ -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
- return Element(self._client, node)
78
+ yield Element(self._client, node)
43
79
  node = node.parent
44
80
  elif node:
45
- return Element(self._client, node)
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
- # field select, textearea...
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._formdata:
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 button(self, text: str) -> "WebForm":
83
- assert self._form.by_text(text, node_name="button") is not None
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, data=self._formdata, follow_redirects=follow_redirects
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, url: str, data: Mapping[str, Any], follow_redirects: bool = True
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
  )
@@ -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
- name: Optional[str] = Query(...),
14
- token: Optional[str] = Query(...),
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
- data = await reg.renderer.pydantic_form(model_cls, None, name, token, removable)
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.2.3
3
+ Version: 0.3.1
4
4
  Summary: High-level web framework
5
5
  License: BSD-derived
6
6
  Author: Guillaume Gauvrit
@@ -18,9 +18,9 @@ Classifier: Topic :: Internet :: WWW/HTTP
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Dist: beautifulsoup4[testing] (>=4.12.2,<5.0.0)
20
20
  Requires-Dist: behave (>=1.2.6,<2.0.0)
21
- Requires-Dist: fastapi (>=0.108.0,<0.109.0)
21
+ Requires-Dist: fastapi (>=0.110.0,<0.111.0)
22
22
  Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
23
- Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
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.1.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
61
+ fastlifeweb-0.3.1.dist-info/METADATA,sha256=uQeIQW_FI5D-pzVE6GkWSJEXDbPIEdbWPIg83GQx448,1748
62
+ fastlifeweb-0.3.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
63
+ fastlifeweb-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,2 +0,0 @@
1
- {%- from "globals.jinja2" import form, link, button, checkbox, title, input -%}
2
- {%- block base %}{% endblock -%}
@@ -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 %}