fastlifeweb 0.20.1__py3-none-any.whl → 0.21.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.
Files changed (54) hide show
  1. CHANGELOG.md +10 -0
  2. fastlife/__init__.py +5 -0
  3. fastlife/adapters/jinjax/renderer.py +4 -52
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
  7. fastlife/adapters/jinjax/widget_factory/factory.py +20 -21
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
  15. fastlife/adapters/jinjax/widgets/base.py +35 -35
  16. fastlife/adapters/jinjax/widgets/boolean.py +13 -34
  17. fastlife/adapters/jinjax/widgets/checklist.py +36 -42
  18. fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
  19. fastlife/adapters/jinjax/widgets/hidden.py +7 -15
  20. fastlife/adapters/jinjax/widgets/model.py +33 -40
  21. fastlife/adapters/jinjax/widgets/sequence.py +61 -40
  22. fastlife/adapters/jinjax/widgets/text.py +39 -78
  23. fastlife/adapters/jinjax/widgets/union.py +50 -57
  24. fastlife/components/CsrfToken.jinja +1 -1
  25. fastlife/components/pydantic_form/Widget.jinja +4 -3
  26. fastlife/config/configurator.py +65 -19
  27. fastlife/config/exceptions.py +0 -2
  28. fastlife/config/views.py +0 -2
  29. fastlife/domain/__init__.py +1 -0
  30. fastlife/domain/model/__init__.py +1 -0
  31. fastlife/domain/model/security.py +19 -0
  32. fastlife/domain/model/template.py +30 -0
  33. fastlife/domain/model/types.py +17 -0
  34. fastlife/request/request.py +19 -0
  35. fastlife/security/csrf.py +3 -13
  36. fastlife/services/templates.py +9 -42
  37. fastlife/services/translations.py +12 -0
  38. fastlife/templates/__init__.py +1 -6
  39. fastlife/templates/inline.py +18 -14
  40. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/METADATA +1 -1
  41. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/RECORD +44 -49
  42. fastlife/components/pydantic_form/Boolean.jinja +0 -13
  43. fastlife/components/pydantic_form/Checklist.jinja +0 -21
  44. fastlife/components/pydantic_form/Dropdown.jinja +0 -18
  45. fastlife/components/pydantic_form/Hidden.jinja +0 -3
  46. fastlife/components/pydantic_form/Model.jinja +0 -30
  47. fastlife/components/pydantic_form/Sequence.jinja +0 -47
  48. fastlife/components/pydantic_form/Text.jinja +0 -11
  49. fastlife/components/pydantic_form/Textarea.jinja +0 -38
  50. fastlife/components/pydantic_form/Union.jinja +0 -34
  51. fastlife/templates/binding.py +0 -52
  52. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/WHEEL +0 -0
  53. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/entry_points.txt +0 -0
  54. {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,8 +3,9 @@ Widget for field of type Set.
3
3
  """
4
4
 
5
5
  from collections.abc import Sequence
6
+ from typing import Self
6
7
 
7
- from pydantic import BaseModel, Field
8
+ from pydantic import BaseModel, Field, model_validator
8
9
 
9
10
  from .base import Widget
10
11
 
@@ -19,52 +20,45 @@ class Checkable(BaseModel):
19
20
  checked: bool
20
21
  error: str | None = Field(default=None)
21
22
 
22
- @property
23
- def id(self) -> str:
24
- id = f"{self.name}-{self.value}-{self.token}"
25
- return id.replace(".", "-").replace("_", "-")
23
+ id: str | None = Field(default=None)
24
+ field_name: str | None = Field(default=None)
26
25
 
27
- @property
28
- def field_name(self) -> str:
29
- return f"{self.name}[]"
26
+ @model_validator(mode="after")
27
+ def fill_props(self) -> Self:
28
+ self.id = f"{self.name}-{self.value}-{self.token}".replace(".", "-")
29
+ self.field_name = f"{self.name}[]"
30
+ return self
30
31
 
31
32
 
32
33
  class ChecklistWidget(Widget[Sequence[Checkable]]):
33
34
  """
34
35
  Widget for field of type Set.
35
-
36
- :param name: field name.
37
- :param title: title for the widget.
38
- :param hint: hint for human.
39
- :param aria_label: html input aria-label value.
40
- :param value: current value.
41
- :param error: error of the value if any.
42
- :param removable: display a button to remove the widget for optional fields.
43
- :param token: token used to get unique id on the form.
44
36
  """
45
37
 
46
- def __init__(
47
- self,
48
- name: str,
49
- *,
50
- title: str | None,
51
- hint: str | None = None,
52
- aria_label: str | None = None,
53
- value: Sequence[Checkable],
54
- error: str | None = None,
55
- token: str,
56
- removable: bool,
57
- ) -> None:
58
- super().__init__(
59
- name,
60
- value=value,
61
- error=error,
62
- token=token,
63
- title=title,
64
- hint=hint,
65
- aria_label=aria_label,
66
- removable=removable,
67
- )
68
-
69
- def get_template(self) -> str:
70
- return "pydantic_form.Checklist.jinja"
38
+ template = """
39
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
40
+ <div class="pt-4">
41
+ <Details>
42
+ <Summary :id="id + '-summary'">
43
+ <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
44
+ <pydantic_form.Error :text="error" />
45
+ </Summary>
46
+ <div>
47
+ {% for value in value %}
48
+ <div class="flex items-center mb-4">
49
+ <Checkbox :name="value.field_name" type="checkbox"
50
+ :id="value.id"
51
+ :value="value.value"
52
+ :checked="value.checked" />
53
+ <Label :for="value.id"
54
+ class="ms-2 text-base text-neutral-900 dark:text-white">
55
+ {{- value.label -}}
56
+ </Label>
57
+ <pydantic_form.Error :text="value.error" />
58
+ </div>
59
+ {% endfor %}
60
+ </div>
61
+ </Details>
62
+ </div>
63
+ </pydantic_form.Widget>
64
+ """
@@ -4,53 +4,47 @@ Widget for field of type Enum or Literal.
4
4
 
5
5
  from collections.abc import Sequence
6
6
 
7
+ from pydantic import Field, field_validator
8
+
7
9
  from .base import Widget
8
10
 
9
11
 
10
12
  class DropDownWidget(Widget[str]):
11
13
  """
12
14
  Widget for field of type Enum or Literal.
15
+ """
13
16
 
14
- :param name: field name.
15
- :param title: title for the widget.
16
- :param hint: hint for human.
17
- :param aria_label: html input aria-label value.
18
- :param value: current value.
19
- :param error: error of the value if any.
20
- :param options: List of possible values.
21
- :param removable: display a button to remove the widget for optional fields.
22
- :param token: token used to get unique id on the form.
17
+ template = """
18
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
19
+ <div class="pt-4">
20
+ <Label :for="id">{{title}}</Label>
21
+ <Select :name="name" :id="id">
22
+ {%- for opt in options -%}
23
+ <Option :value="opt.value" id={{id + "-" + opt.value.replace(" ", " -")}}
24
+ :selected="value==opt.value">
25
+ {{- opt.text -}}
26
+ </Option>
27
+ {%- endfor -%}
28
+ </Select>
29
+ <pydantic_form.Error :text="error" />
30
+ <pydantic_form.Hint :text="hint" />
31
+ </div>
32
+ </pydantic_form.Widget>
23
33
  """
24
34
 
25
- def __init__(
26
- self,
27
- name: str,
28
- *,
29
- title: str | None,
30
- hint: str | None = None,
31
- aria_label: str | None = None,
32
- value: str | None = None,
33
- error: str | None = None,
34
- options: Sequence[tuple[str, str]] | Sequence[str],
35
- removable: bool = False,
36
- token: str | None = None,
37
- ) -> None:
38
- super().__init__(
39
- name,
40
- value=value,
41
- error=error,
42
- title=title,
43
- token=token,
44
- removable=removable,
45
- hint=hint,
46
- aria_label=aria_label,
47
- )
48
- self.options: list[dict[str, str]] = []
35
+ options: list[dict[str | int, str | int]] = Field(default_factory=list)
36
+
37
+ @field_validator("options", mode="before")
38
+ @classmethod
39
+ def validate_options(
40
+ cls, options: Sequence[str | int | tuple[str | int, str | int]] | None
41
+ ) -> Sequence[dict[str | int, str | int]]:
42
+ if not options:
43
+ return []
44
+ ret: list[dict[str | int, str | int]] = []
49
45
  for opt in options:
50
46
  if isinstance(opt, tuple):
51
- self.options.append({"value": opt[0], "text": opt[1]})
47
+ ret.append({"value": opt[0], "text": opt[1]})
52
48
  else:
53
- self.options.append({"value": opt, "text": opt})
54
-
55
- def get_template(self) -> str:
56
- return "pydantic_form.Dropdown.jinja"
49
+ ret.append({"value": opt, "text": opt})
50
+ return ret
@@ -1,33 +1,25 @@
1
1
  """Hidden fields"""
2
2
 
3
- from typing import Any
3
+ from fastlife.domain.model.types import Builtins
4
4
 
5
5
  from .base import Widget
6
6
 
7
7
 
8
- class HiddenWidget(Widget[str]):
8
+ class HiddenWidget(Widget[Builtins]):
9
9
  '''
10
10
  Widget to annotate to display a field as an hidden field.
11
11
 
12
12
  ::
13
13
  from pydantic import BaseModel
14
+ from fastlife.adapters.jinjax.widgets.base import CustomWidget
14
15
  from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
15
16
 
16
17
  class MyForm(BaseModel):
17
- id: Annotated[str, HiddenWidget] = Field(...)
18
+ id: Annotated[str, CustomWidget(HiddenWidget)] = Field(...)
18
19
  """Identifier in the database."""
19
20
 
20
21
  '''
21
22
 
22
- def __init__(
23
- self,
24
- name: str,
25
- *,
26
- value: str,
27
- token: str,
28
- **kwargs: Any,
29
- ) -> None:
30
- super().__init__(name, value=value, token=token)
31
-
32
- def get_template(self) -> str:
33
- return "pydantic_form.Hidden.jinja"
23
+ template = """
24
+ <Hidden :name="name" :value="value" :id="id" />
25
+ """
@@ -1,51 +1,44 @@
1
1
  """Pydantic models"""
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import Any
5
4
 
6
5
  from markupsafe import Markup
6
+ from pydantic import Field
7
7
 
8
8
  from fastlife.services.templates import AbstractTemplateRenderer
9
9
 
10
- from .base import Widget
11
-
12
-
13
- class ModelWidget(Widget[Sequence[Widget[Any]]]):
14
- def __init__(
15
- self,
16
- name: str,
17
- *,
18
- value: Sequence[Widget[Any]],
19
- error: str | None = None,
20
- removable: bool,
21
- title: str,
22
- hint: str | None = None,
23
- aria_label: str | None = None,
24
- token: str,
25
- nested: bool,
26
- ):
27
- super().__init__(
28
- name,
29
- title=title,
30
- hint=hint,
31
- aria_label=aria_label,
32
- value=value,
33
- error=error,
34
- removable=removable,
35
- token=token,
36
- )
37
- self.nested = nested
38
-
39
- def get_template(self) -> str:
40
- return "pydantic_form.Model.jinja"
10
+ from .base import TWidget, Widget
11
+
12
+
13
+ class ModelWidget(Widget[Sequence[TWidget]]):
14
+ template = """
15
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
16
+ <div id="{{id}}"{% if nested %} class="m-4"{%endif%}>
17
+ {% if nested %}
18
+ <Details>
19
+ <Summary :id="id + '-summary'">
20
+ <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
21
+ <pydantic_form.Error :text="error" />
22
+ </Summary>
23
+ <div>
24
+ {% for child in children_widgets %}
25
+ {{ child }}
26
+ {% endfor %}
27
+ </div>
28
+ </Details>
29
+ {% else %}
30
+ {% for child in children_widgets %}
31
+ {{ child }}
32
+ {% endfor %}
33
+ {% endif %}
34
+ </div>
35
+ </pydantic_form.Widget>
36
+ """
37
+
38
+ nested: bool = Field(default=False)
39
+ children_widgets: list[str] | None = Field(default=None)
41
40
 
42
41
  def to_html(self, renderer: AbstractTemplateRenderer) -> Markup:
43
42
  """Return the html version."""
44
- children_widget = [child.to_html(renderer) for child in self.value or []]
45
- kwargs = {
46
- "widget": self,
47
- "children_widget": children_widget,
48
- }
49
- return Markup(
50
- renderer.render_template(self.get_template(), globals=None, **kwargs)
51
- )
43
+ self.children_widgets = [child.to_html(renderer) for child in self.value or []]
44
+ return Markup(renderer.render_template(self))
@@ -2,52 +2,73 @@ from collections.abc import Sequence
2
2
  from typing import Any
3
3
 
4
4
  from markupsafe import Markup
5
+ from pydantic import Field
5
6
 
6
7
  from fastlife.services.templates import AbstractTemplateRenderer
7
8
 
8
- from .base import TypeWrapper, Widget
9
-
10
-
11
- class SequenceWidget(Widget[Sequence[Widget[Any]]]):
12
- def __init__(
13
- self,
14
- name: str,
15
- *,
16
- title: str | None,
17
- hint: str | None = None,
18
- aria_label: str | None = None,
19
- value: Sequence[Widget[Any]] | None,
20
- error: str | None = None,
21
- item_type: type[Any],
22
- token: str,
23
- removable: bool,
24
- ):
25
- super().__init__(
26
- name,
27
- value=value,
28
- error=error,
29
- title=title,
30
- hint=hint,
31
- aria_label=aria_label,
32
- token=token,
33
- removable=removable,
34
- )
35
- self.item_type = item_type
36
-
37
- def get_template(self) -> str:
38
- return "pydantic_form.Sequence.jinja"
9
+ from .base import TWidget, TypeWrapper, Widget
10
+
11
+
12
+ class SequenceWidget(Widget[Sequence[TWidget]]):
13
+ template = """
14
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
15
+ <Details :id="id">
16
+ <Summary :id="id + '-summary'">
17
+ <H3 :class="H3_SUMMARY_CLASS">{{title}}</H3>
18
+ <pydantic_form.Error :text="error" />
19
+ </Summary>
20
+ <div>
21
+ {% set fnGetName = "get" + id.replace("-", "_") %}
22
+ <script>
23
+ function {{ fnGetName }} () {
24
+ const el = document.getElementById("{{id}}-content");
25
+ const len = el.dataset.length;
26
+ el.dataset.length = parseInt(len) + 1;
27
+ return "{{wrapped_type.name}}." + len;
28
+ }
29
+ </script>
30
+
31
+ <div id="{{id}}-content" class="m-4"
32
+ data-length="{{children_widgets|length|string}}">
33
+ {% set container_id = id + "-children-container" %}
34
+ <div id="{{container_id}}">
35
+ {% for child in children_widgets %}
36
+ {{ child }}
37
+ {% endfor%}
38
+ </div>
39
+ </div>
40
+
41
+ <div>
42
+ {% set container_id = "#" + id + "-children-container" %}
43
+ {% set add_id = id + "-add" %}
44
+ {% set vals = 'js:{"name": '
45
+ + fnGetName
46
+ + '(), "token": "'
47
+ + wrapped_type.token + '", "removable": true}' %}
48
+ <Button type="button"
49
+ :hx-target="container_id" hx-swap="beforeend"
50
+ :id="add_id"
51
+ :hx-vals="vals"
52
+ :hx-get="wrapped_type.url">
53
+ Add
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ </Details>
58
+ </pydantic_form.Widget>
59
+ """
60
+
61
+ item_type: type[Any]
62
+ wrapped_type: TypeWrapper | None = Field(default=None)
63
+ children_widgets: list[str] = Field(default=None)
39
64
 
40
65
  def build_item_type(self, route_prefix: str) -> TypeWrapper:
41
66
  return TypeWrapper(self.item_type, route_prefix, self.name, self.token)
42
67
 
43
68
  def to_html(self, renderer: "AbstractTemplateRenderer") -> Markup:
44
69
  """Return the html version."""
45
- children = [Markup(item.to_html(renderer)) for item in self.value or []]
46
- return Markup(
47
- renderer.render_template(
48
- self.get_template(),
49
- widget=self,
50
- type=self.build_item_type(renderer.route_prefix),
51
- children_widgets=children,
52
- )
53
- )
70
+ self.wrapped_type = self.build_item_type(renderer.route_prefix)
71
+ self.children_widgets = [
72
+ Markup(item.to_html(renderer)) for item in self.value or []
73
+ ]
74
+ return Markup(renderer.render_template(self))
@@ -1,52 +1,31 @@
1
1
  from collections.abc import Sequence
2
2
 
3
+ from pydantic import Field
4
+
5
+ from fastlife.domain.model.types import Builtins
6
+
3
7
  from .base import Widget
4
8
 
5
9
 
6
- class TextWidget(Widget[str]):
10
+ class TextWidget(Widget[Builtins]):
7
11
  """
8
12
  Widget for text like field (email, ...).
9
-
10
- :param name: input name.
11
- :param title: title for the widget.
12
- :param hint: hint for human.
13
- :param aria_label: html input aria-label value.
14
- :param placeholder: html input placeholder value.
15
- :param error: error of the value if any.
16
- :param value: current value.
17
- :param removable: display a button to remove the widget for optional fields.
18
- :param token: token used to get unique id on the form.
19
13
  """
20
14
 
21
- def __init__(
22
- self,
23
- name: str,
24
- *,
25
- title: str | None,
26
- hint: str | None = None,
27
- aria_label: str | None = None,
28
- placeholder: str | None = None,
29
- error: str | None = None,
30
- value: str = "",
31
- input_type: str = "text",
32
- removable: bool = False,
33
- token: str,
34
- ) -> None:
35
- super().__init__(
36
- name,
37
- value=value,
38
- title=title,
39
- hint=hint,
40
- aria_label=aria_label,
41
- token=token,
42
- error=error,
43
- removable=removable,
44
- )
45
- self.placeholder = placeholder or ""
46
- self.input_type = input_type
15
+ template = """
16
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
17
+ <div class="pt-4">
18
+ <Label :for="id">{{title}}</Label>
19
+ <pydantic_form.Error :text="error" />
20
+ <Input :name="name" :value="value" :type="input_type" :id="id"
21
+ :aria-label="aria_label" :placeholder="placeholder" />
22
+ <pydantic_form.Hint :text="hint" />
23
+ </div>
24
+ </pydantic_form.Widget>
25
+ """
47
26
 
48
- def get_template(self) -> str:
49
- return "pydantic_form.Text.jinja"
27
+ input_type: str = Field(default="text")
28
+ placeholder: str | None = Field(default=None)
50
29
 
51
30
 
52
31
  class TextareaWidget(Widget[Sequence[str]]):
@@ -54,12 +33,13 @@ class TextareaWidget(Widget[Sequence[str]]):
54
33
  Render a Textearea for a string or event a sequence of string.
55
34
 
56
35
  ```
36
+ from fastlife.adapters.jinjax.widgets.base import CustomWidget
57
37
  from fastlife.adapters.jinjax.widgets.text import TextareaWidget
58
38
  from pydantic import BaseModel, Field, field_validator
59
39
 
60
40
  class TaggedParagraphForm(BaseModel):
61
- paragraph: Annotated[str, TextareaWidget] = Field(...)
62
- tags: Annotated[Sequence[str], TextareaWidget] = Field(
41
+ paragraph: Annotated[str, CustomWidget(TextareaWidget)] = Field(...)
42
+ tags: Annotated[Sequence[str], CustomWidget(TextareaWidget)] = Field(
63
43
  default_factory=list,
64
44
  title="Tags",
65
45
  description="One tag per line",
@@ -69,43 +49,24 @@ class TextareaWidget(Widget[Sequence[str]]):
69
49
  def split(cls, s: Any) -> Sequence[str]:
70
50
  return s.split() if s else []
71
51
  ```
72
-
73
- :param name: input name.
74
- :param title: title for the widget.
75
- :param hint: hint for human.
76
- :param aria_label: html input aria-label value.
77
- :param placeholder: html input placeholder value.
78
- :param error: error of the value if any.
79
- :param value: current value.
80
- :param removable: display a button to remove the widget for optional fields.
81
- :param token: token used to get unique id on the form.
82
-
83
52
  """
84
53
 
85
- def __init__(
86
- self,
87
- name: str,
88
- *,
89
- title: str | None,
90
- hint: str | None = None,
91
- aria_label: str | None = None,
92
- placeholder: str | None = None,
93
- error: str | None = None,
94
- value: Sequence[str] | None = None,
95
- removable: bool = False,
96
- token: str,
97
- ) -> None:
98
- super().__init__(
99
- name,
100
- value=value or [],
101
- title=title,
102
- hint=hint,
103
- aria_label=aria_label,
104
- token=token,
105
- error=error,
106
- removable=removable,
107
- )
108
- self.placeholder = placeholder or ""
54
+ template = """
55
+ <pydantic_form.Widget :widget_id="id" :removable="removable">
56
+ <div class="pt-4">
57
+ <Label :for="id">{{title}}</Label>
58
+ <pydantic_form.Error :text="error" />
59
+ <Textarea :name="name" :id="id" :aria-label="aria_label">
60
+ {%- if v is string -%}
61
+ {{- v -}}}
62
+ {%- else -%}
63
+ {%- for v in value %}{{v}}
64
+ {% endfor -%}
65
+ {% endif %}
66
+ </Textarea>
67
+ <pydantic_form.Hint :text="hint" />
68
+ </div>
69
+ </pydantic_form.Widget>
70
+ """
109
71
 
110
- def get_template(self) -> str:
111
- return "pydantic_form.Textarea.jinja"
72
+ placeholder: str = Field(default="")