fastlifeweb 0.27.0__py3-none-any.whl → 0.28.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 (56) hide show
  1. CHANGELOG.md +11 -0
  2. fastlife/__init__.py +2 -1
  3. fastlife/adapters/jinjax/renderer.py +8 -0
  4. fastlife/adapters/jinjax/widgets/union.py +1 -1
  5. fastlife/adapters/xcomponent/__init__.py +1 -0
  6. fastlife/adapters/xcomponent/catalog.py +11 -0
  7. fastlife/adapters/xcomponent/html/__init__.py +7 -0
  8. fastlife/adapters/xcomponent/html/collapsible.py +76 -0
  9. fastlife/adapters/xcomponent/html/form.py +437 -0
  10. fastlife/adapters/xcomponent/html/nav.py +60 -0
  11. fastlife/adapters/xcomponent/html/table.py +130 -0
  12. fastlife/adapters/xcomponent/html/text.py +30 -0
  13. fastlife/adapters/xcomponent/html/title.py +145 -0
  14. fastlife/adapters/xcomponent/icons/__init__.py +0 -0
  15. fastlife/adapters/xcomponent/icons/icons.py +93 -0
  16. fastlife/adapters/xcomponent/pydantic_form/__init__.py +0 -0
  17. fastlife/adapters/xcomponent/pydantic_form/components.py +121 -0
  18. fastlife/adapters/xcomponent/pydantic_form/widget_factory/__init__.py +1 -0
  19. fastlife/adapters/xcomponent/pydantic_form/widget_factory/base.py +40 -0
  20. fastlife/adapters/xcomponent/pydantic_form/widget_factory/bool_builder.py +45 -0
  21. fastlife/adapters/xcomponent/pydantic_form/widget_factory/emailstr_builder.py +50 -0
  22. fastlife/adapters/xcomponent/pydantic_form/widget_factory/enum_builder.py +49 -0
  23. fastlife/adapters/xcomponent/pydantic_form/widget_factory/factory.py +188 -0
  24. fastlife/adapters/xcomponent/pydantic_form/widget_factory/literal_builder.py +55 -0
  25. fastlife/adapters/xcomponent/pydantic_form/widget_factory/model_builder.py +66 -0
  26. fastlife/adapters/xcomponent/pydantic_form/widget_factory/secretstr_builder.py +48 -0
  27. fastlife/adapters/xcomponent/pydantic_form/widget_factory/sequence_builder.py +60 -0
  28. fastlife/adapters/xcomponent/pydantic_form/widget_factory/set_builder.py +85 -0
  29. fastlife/adapters/xcomponent/pydantic_form/widget_factory/simpletype_builder.py +48 -0
  30. fastlife/adapters/xcomponent/pydantic_form/widget_factory/union_builder.py +92 -0
  31. fastlife/adapters/xcomponent/pydantic_form/widgets/__init__.py +1 -0
  32. fastlife/adapters/xcomponent/pydantic_form/widgets/base.py +140 -0
  33. fastlife/adapters/xcomponent/pydantic_form/widgets/boolean.py +25 -0
  34. fastlife/adapters/xcomponent/pydantic_form/widgets/checklist.py +75 -0
  35. fastlife/adapters/xcomponent/pydantic_form/widgets/dropdown.py +72 -0
  36. fastlife/adapters/xcomponent/pydantic_form/widgets/hidden.py +25 -0
  37. fastlife/adapters/xcomponent/pydantic_form/widgets/mfa_code.py +25 -0
  38. fastlife/adapters/xcomponent/pydantic_form/widgets/model.py +49 -0
  39. fastlife/adapters/xcomponent/pydantic_form/widgets/sequence.py +74 -0
  40. fastlife/adapters/xcomponent/pydantic_form/widgets/text.py +121 -0
  41. fastlife/adapters/xcomponent/pydantic_form/widgets/union.py +81 -0
  42. fastlife/adapters/xcomponent/renderer.py +130 -0
  43. fastlife/assets/dist.css +4 -1
  44. fastlife/components/A.jinja +5 -1
  45. fastlife/config/configurator.py +7 -8
  46. fastlife/config/resources.py +9 -3
  47. fastlife/domain/model/template.py +6 -0
  48. fastlife/service/csrf.py +1 -1
  49. fastlife/service/templates.py +44 -2
  50. fastlife/template_globals.py +3 -0
  51. fastlife/views/pydantic_form.py +9 -9
  52. {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/METADATA +6 -3
  53. {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/RECORD +56 -18
  54. {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/WHEEL +1 -1
  55. {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/entry_points.txt +0 -0
  56. {fastlifeweb-0.27.0.dist-info → fastlifeweb-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,49 @@
1
+ """Handle Enum type."""
2
+
3
+ from collections.abc import Mapping
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+ from pydantic.fields import FieldInfo
8
+
9
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
10
+ BaseWidgetBuilder,
11
+ )
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
13
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.dropdown import DropDownWidget
14
+
15
+
16
+ class EnumBuilder(BaseWidgetBuilder[Enum]):
17
+ """Builder for Enum."""
18
+
19
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
20
+ """True for Enum."""
21
+ return issubclass(typ, Enum)
22
+
23
+ def build(
24
+ self,
25
+ *,
26
+ field_name: str,
27
+ field_type: type[Any], # an enum subclass
28
+ field: FieldInfo | None,
29
+ value: Enum | None, # str | int | float,
30
+ form_errors: Mapping[str, Any],
31
+ removable: bool,
32
+ ) -> Widget[Enum]:
33
+ """Build the widget."""
34
+ options = [(item.name, item.value) for item in field_type] # type: ignore
35
+ return DropDownWidget(
36
+ name=field_name,
37
+ options=options, # type: ignore
38
+ removable=removable,
39
+ title=field.title or "" if field else "",
40
+ hint=field.description if field else None,
41
+ aria_label=(
42
+ field.json_schema_extra.get("aria_label") # type:ignore
43
+ if field and field.json_schema_extra
44
+ else None
45
+ ),
46
+ token=self.factory.token,
47
+ value=str(value),
48
+ error=form_errors.get(field_name),
49
+ )
@@ -0,0 +1,188 @@
1
+ """
2
+ Create markup for pydantic forms.
3
+ """
4
+
5
+ import secrets
6
+ from collections.abc import Mapping
7
+ from typing import TYPE_CHECKING, Any, get_origin
8
+
9
+ from markupsafe import Markup
10
+ from pydantic.fields import FieldInfo
11
+
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import CustomWidget, Widget
13
+ from fastlife.domain.model.form import FormModel
14
+ from fastlife.domain.model.template import XTemplate
15
+
16
+ if TYPE_CHECKING:
17
+ from fastlife.service.templates import AbstractTemplateRenderer
18
+
19
+ from .base import BaseWidgetBuilder
20
+ from .bool_builder import BoolBuilder
21
+ from .emailstr_builder import EmailStrBuilder
22
+ from .enum_builder import EnumBuilder
23
+ from .literal_builder import LiteralBuilder
24
+ from .model_builder import ModelBuilder
25
+ from .secretstr_builder import SecretStrBuilder
26
+ from .sequence_builder import SequenceBuilder
27
+ from .set_builder import SetBuilder
28
+ from .simpletype_builder import SimpleTypeBuilder
29
+ from .union_builder import UnionBuilder
30
+
31
+
32
+ class OptionalFatalError(XTemplate):
33
+ template = """
34
+ <>
35
+ {
36
+ if message {
37
+ <FatalError message={message} />
38
+ }
39
+ }
40
+ </>
41
+ """
42
+ message: str
43
+
44
+
45
+ class WidgetFactory:
46
+ """
47
+ Form builder for pydantic model.
48
+
49
+ :param renderer: template engine to render widget.
50
+ :param token: reuse a token.
51
+ """
52
+
53
+ def __init__(self, renderer: "AbstractTemplateRenderer", token: str | None = None):
54
+ self.renderer = renderer
55
+ self.token = token or secrets.token_urlsafe(4).replace("_", "-")
56
+ self.builders: list[BaseWidgetBuilder[Any]] = [
57
+ # Order is super important here
58
+ # starts by the union type
59
+ UnionBuilder(self),
60
+ # and to other types that have an origin
61
+ SetBuilder(self),
62
+ LiteralBuilder(self),
63
+ SequenceBuilder(self),
64
+ # from this part, order does not really matter
65
+ ModelBuilder(self),
66
+ BoolBuilder(self),
67
+ EnumBuilder(self),
68
+ EmailStrBuilder(self),
69
+ SecretStrBuilder(self),
70
+ # we keep simple types, str, int at the end
71
+ SimpleTypeBuilder(self),
72
+ ]
73
+
74
+ def get_markup(
75
+ self,
76
+ model: FormModel[Any],
77
+ *,
78
+ removable: bool = False,
79
+ field: FieldInfo | None = None,
80
+ ) -> Markup:
81
+ """
82
+ Get the markup for the given model.
83
+
84
+ :param model: the model to build the html markup.
85
+ :param removable: Include a button to remove the model in the markup.
86
+ :param field: only build the markup of this field is not None.
87
+ """
88
+ ret = Markup()
89
+ if model.fatal_error:
90
+ ret += Markup(
91
+ self.renderer.render_template(
92
+ OptionalFatalError(message=model.fatal_error)
93
+ )
94
+ )
95
+ ret += Markup(
96
+ self.get_widget(
97
+ model.model.__class__,
98
+ model.form_data,
99
+ model.errors,
100
+ prefix=model.prefix,
101
+ removable=removable,
102
+ field=field,
103
+ ).to_html(self.renderer)
104
+ )
105
+ return ret
106
+
107
+ def get_widget(
108
+ self,
109
+ base: type[Any],
110
+ form_data: Mapping[str, Any],
111
+ form_errors: Mapping[str, Any],
112
+ *,
113
+ prefix: str,
114
+ removable: bool,
115
+ field: FieldInfo | None = None,
116
+ ) -> Widget[Any]:
117
+ """
118
+ build the widget for the given type and return it.
119
+ :param base: the type to build, it has to be a builtin or a Pydantic model.
120
+ :param form_data: form values to render.
121
+ :param form_errors: form errors to render.
122
+ """
123
+ return self.build(
124
+ base,
125
+ value=form_data.get(prefix, {}),
126
+ form_errors=form_errors,
127
+ name=prefix,
128
+ removable=removable,
129
+ field=field,
130
+ )
131
+
132
+ def build(
133
+ self,
134
+ typ: type[Any],
135
+ *,
136
+ name: str = "",
137
+ value: Any,
138
+ removable: bool,
139
+ form_errors: Mapping[str, Any],
140
+ field: FieldInfo | None = None,
141
+ ) -> Widget[Any]:
142
+ """
143
+ build widget tree for the given type.
144
+ This function is recurive and shoud not be used directly.
145
+ The type is a composite, it can be pydantic model, builtin, list or unions.
146
+
147
+ The {meth}`WidgetFactory.get_widget` or {meth}`WidgetFactory.get_markup`
148
+ should be used.
149
+
150
+ :param typ: the type to build, it has to be a builtin or a Pydantic model.
151
+ :param name: name of the widget to build.
152
+ :param value: value for the widget.
153
+ :param removable: True if it has to include a remove button.
154
+ :param form_errors: errors in the form.
155
+ :param field: field information used to customize the widget.
156
+ """
157
+ if field and field.metadata:
158
+ for widget in field.metadata:
159
+ if isinstance(widget, CustomWidget):
160
+ ret: Widget[Any] = widget.typ(
161
+ name=name,
162
+ value=value,
163
+ removable=removable,
164
+ title=field.title or "" if field else "",
165
+ hint=field.description if field else None,
166
+ aria_label=(
167
+ field.json_schema_extra.get("aria_label") # type:ignore
168
+ if field and field.json_schema_extra
169
+ else None
170
+ ),
171
+ token=self.token,
172
+ error=form_errors.get(name),
173
+ )
174
+ return ret
175
+
176
+ type_origin = get_origin(typ)
177
+ for builder in self.builders:
178
+ if builder.accept(typ, type_origin):
179
+ return builder.build(
180
+ field_name=name,
181
+ field_type=typ,
182
+ field=field,
183
+ value=value,
184
+ form_errors=form_errors,
185
+ removable=removable,
186
+ )
187
+
188
+ raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
@@ -0,0 +1,55 @@
1
+ """Handle Literal type."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, Literal
5
+
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
9
+ BaseWidgetBuilder,
10
+ )
11
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.dropdown import DropDownWidget
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.hidden import HiddenWidget
13
+ from fastlife.domain.model.types import AnyLiteral
14
+
15
+
16
+ class LiteralBuilder(BaseWidgetBuilder[AnyLiteral]):
17
+ """Builder for Literal."""
18
+
19
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
20
+ """True for Literal."""
21
+ return origin is Literal
22
+
23
+ def build(
24
+ self,
25
+ *,
26
+ field_name: str,
27
+ field_type: type[Any], # a literal actually
28
+ field: FieldInfo | None,
29
+ value: AnyLiteral | None,
30
+ form_errors: Mapping[str, Any],
31
+ removable: bool,
32
+ ) -> HiddenWidget | DropDownWidget:
33
+ """Build the widget."""
34
+ choices: list[str] = field_type.__args__ # type: ignore
35
+ if len(choices) == 1:
36
+ return HiddenWidget(
37
+ name=field_name,
38
+ value=choices[0],
39
+ token=self.factory.token,
40
+ )
41
+ return DropDownWidget(
42
+ name=field_name,
43
+ options=choices, # type: ignore
44
+ removable=removable,
45
+ title=field.title or "" if field else "",
46
+ hint=field.description if field else None,
47
+ aria_label=(
48
+ field.json_schema_extra.get("aria_label") # type:ignore
49
+ if field and field.json_schema_extra
50
+ else None
51
+ ),
52
+ token=self.factory.token,
53
+ value=str(value),
54
+ error=form_errors.get(field_name),
55
+ )
@@ -0,0 +1,66 @@
1
+ """Handle Pydantic BaseModel type."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel
7
+ from pydantic.fields import FieldInfo
8
+
9
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
10
+ BaseWidgetBuilder,
11
+ )
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
13
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.model import ModelWidget
14
+
15
+
16
+ class ModelBuilder(BaseWidgetBuilder[Mapping[str, Any]]):
17
+ """Builder for Pydantic BaseModel values."""
18
+
19
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
20
+ """True for Pydantic BaseModel."""
21
+ return issubclass(typ, BaseModel)
22
+
23
+ def build(
24
+ self,
25
+ *,
26
+ field_name: str,
27
+ field_type: type[BaseModel],
28
+ field: FieldInfo | None,
29
+ value: Mapping[str, Any] | None,
30
+ form_errors: Mapping[str, Any],
31
+ removable: bool,
32
+ ) -> Widget[Any]:
33
+ """Build the widget."""
34
+ value = value or {}
35
+ ret: dict[str, Any] = {}
36
+ for key, child_field in field_type.model_fields.items():
37
+ child_key = f"{field_name}.{key}" if field_name else key
38
+ if child_field.exclude:
39
+ continue
40
+ if child_field.annotation is None:
41
+ raise ValueError( # coverage: ignore
42
+ f"Missing annotation for {child_field} in {child_key}"
43
+ )
44
+ ret[key] = self.factory.build(
45
+ child_field.annotation,
46
+ name=child_key,
47
+ field=child_field,
48
+ value=value.get(key),
49
+ form_errors=form_errors,
50
+ removable=False,
51
+ )
52
+ return ModelWidget[Any](
53
+ name=field_name,
54
+ value=list(ret.values()),
55
+ removable=removable,
56
+ title=field.title or "" if field and field.title else "",
57
+ hint=field.description if field else None,
58
+ aria_label=(
59
+ field.json_schema_extra.get("aria_label") # type:ignore
60
+ if field and field.json_schema_extra
61
+ else None
62
+ ),
63
+ token=self.factory.token,
64
+ error=form_errors.get(field_name),
65
+ nested=field is not None,
66
+ )
@@ -0,0 +1,48 @@
1
+ """Handle Pydantic SecretStr type."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from pydantic import SecretStr
7
+ from pydantic.fields import FieldInfo
8
+
9
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
10
+ BaseWidgetBuilder,
11
+ )
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
13
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.text import PasswordWidget
14
+
15
+
16
+ class SecretStrBuilder(BaseWidgetBuilder[SecretStr]):
17
+ """Builder for Pydantic SecretStr."""
18
+
19
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
20
+ """True for Pydantic SecretStr."""
21
+ return issubclass(typ, SecretStr)
22
+
23
+ def build(
24
+ self,
25
+ *,
26
+ field_name: str,
27
+ field_type: type[Any],
28
+ field: FieldInfo | None,
29
+ value: SecretStr | None,
30
+ form_errors: Mapping[str, Any],
31
+ removable: bool,
32
+ ) -> Widget[SecretStr]:
33
+ """Build the widget."""
34
+ return PasswordWidget(
35
+ name=field_name,
36
+ placeholder=str(field.examples[0]) if field and field.examples else None,
37
+ removable=removable,
38
+ title=field.title or "" if field else "",
39
+ hint=field.description if field else None,
40
+ aria_label=(
41
+ field.json_schema_extra.get("aria_label") # type:ignore
42
+ if field and field.json_schema_extra
43
+ else None
44
+ ),
45
+ token=self.factory.token,
46
+ error=form_errors.get(field_name),
47
+ new_password="new-password" in field.metadata if field else False,
48
+ )
@@ -0,0 +1,60 @@
1
+ """Handle Sequence type."""
2
+
3
+ from collections.abc import Mapping, MutableSequence, Sequence
4
+ from typing import Any
5
+
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
9
+ BaseWidgetBuilder,
10
+ )
11
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.sequence import SequenceWidget
13
+
14
+
15
+ class SequenceBuilder(BaseWidgetBuilder[Sequence[Any]]):
16
+ """Builder for Sequence values."""
17
+
18
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
19
+ """True for Sequence, MutableSequence or list"""
20
+ return origin is Sequence or origin is MutableSequence or origin is list
21
+
22
+ def build(
23
+ self,
24
+ *,
25
+ field_name: str,
26
+ field_type: type[Any],
27
+ field: FieldInfo | None,
28
+ value: Sequence[Any] | None,
29
+ form_errors: Mapping[str, Any],
30
+ removable: bool,
31
+ ) -> Widget[Sequence[Any]]:
32
+ """Build the widget."""
33
+ typ = field_type.__args__[0] # type: ignore
34
+ value = value or []
35
+ items: Sequence[Any] = [
36
+ self.factory.build(
37
+ typ, # type: ignore
38
+ name=f"{field_name}.{idx}",
39
+ value=v,
40
+ field=field,
41
+ form_errors=form_errors,
42
+ removable=True,
43
+ )
44
+ for idx, v in enumerate(value)
45
+ ]
46
+ return SequenceWidget[Any](
47
+ name=field_name,
48
+ title=field.title or "" if field else "",
49
+ hint=field.description if field else None,
50
+ aria_label=(
51
+ field.json_schema_extra.get("aria_label") # type:ignore
52
+ if field and field.json_schema_extra
53
+ else None
54
+ ),
55
+ value=items,
56
+ item_type=typ, # type: ignore
57
+ token=self.factory.token,
58
+ removable=removable,
59
+ error=form_errors.get(field_name),
60
+ )
@@ -0,0 +1,85 @@
1
+ """Handle Set type."""
2
+
3
+ from collections.abc import Mapping
4
+ from enum import Enum
5
+ from typing import Any, Literal, get_origin
6
+
7
+ from pydantic.fields import FieldInfo
8
+
9
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
10
+ BaseWidgetBuilder,
11
+ )
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
13
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.checklist import (
14
+ Checkable,
15
+ ChecklistWidget,
16
+ )
17
+
18
+
19
+ class SetBuilder(BaseWidgetBuilder[set[Any]]):
20
+ """Builder for Set."""
21
+
22
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
23
+ """True for Set"""
24
+ return origin is set
25
+
26
+ def build(
27
+ self,
28
+ *,
29
+ field_name: str,
30
+ field_type: type[Any],
31
+ field: FieldInfo | None,
32
+ value: set[Any] | None,
33
+ form_errors: Mapping[str, Any],
34
+ removable: bool,
35
+ ) -> Widget[Any]:
36
+ """Build the widget."""
37
+ choice_wrapper = field_type.__args__[0]
38
+ choices = []
39
+ choice_wrapper_origin = get_origin(choice_wrapper)
40
+ if choice_wrapper_origin:
41
+ if choice_wrapper_origin is Literal:
42
+ litchoice: list[str] = choice_wrapper.__args__ # type: ignore
43
+ choices = [
44
+ Checkable(
45
+ label=c,
46
+ value=c,
47
+ checked=c in value if value else False, # type: ignore
48
+ name=field_name,
49
+ token=self.factory.token,
50
+ error=form_errors.get(f"{field_name}-{c}"),
51
+ )
52
+ for c in litchoice
53
+ ]
54
+
55
+ else:
56
+ raise NotImplementedError # coverage: ignore
57
+ elif issubclass(choice_wrapper, Enum):
58
+ choices = [
59
+ Checkable(
60
+ label=e.value,
61
+ value=e.name,
62
+ checked=e.name in value if value else False, # type: ignore
63
+ name=field_name,
64
+ token=self.factory.token,
65
+ error=form_errors.get(f"{field_name}-{e.name}"),
66
+ )
67
+ for e in choice_wrapper
68
+ ]
69
+ else:
70
+ raise NotImplementedError # coverage: ignore
71
+
72
+ return ChecklistWidget(
73
+ name=field_name,
74
+ title=field.title or "" if field else "",
75
+ hint=field.description if field else None,
76
+ aria_label=(
77
+ field.json_schema_extra.get("aria_label") # type:ignore
78
+ if field and field.json_schema_extra
79
+ else None
80
+ ),
81
+ token=self.factory.token,
82
+ value=choices,
83
+ removable=removable,
84
+ error=form_errors.get(field_name),
85
+ )
@@ -0,0 +1,48 @@
1
+ """Handle simple types (str, int, float, ...)."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
9
+ BaseWidgetBuilder,
10
+ )
11
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
12
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.text import TextWidget
13
+ from fastlife.domain.model.types import Builtins
14
+
15
+
16
+ class SimpleTypeBuilder(BaseWidgetBuilder[Builtins]):
17
+ """Builder for simple types."""
18
+
19
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
20
+ """True for simple types: int, str, float, Decimal, UUID"""
21
+ return issubclass(typ, Builtins)
22
+
23
+ def build(
24
+ self,
25
+ *,
26
+ field_name: str,
27
+ field_type: type[Any],
28
+ field: FieldInfo | None,
29
+ value: Builtins | None,
30
+ form_errors: Mapping[str, Any],
31
+ removable: bool,
32
+ ) -> Widget[Builtins]:
33
+ """Build the widget."""
34
+ return TextWidget(
35
+ name=field_name,
36
+ placeholder=str(field.examples[0]) if field and field.examples else None,
37
+ title=field.title or "" if field else "",
38
+ hint=field.description if field else None,
39
+ aria_label=(
40
+ field.json_schema_extra.get("aria_label") # type:ignore
41
+ if field and field.json_schema_extra
42
+ else None
43
+ ),
44
+ removable=removable,
45
+ token=self.factory.token,
46
+ value=str(value) if value else "",
47
+ error=form_errors.get(field_name),
48
+ )
@@ -0,0 +1,92 @@
1
+ """Handle Union type."""
2
+
3
+ from collections.abc import Mapping
4
+ from types import NoneType
5
+ from typing import Any
6
+
7
+ from pydantic import ValidationError
8
+ from pydantic.fields import FieldInfo
9
+
10
+ from fastlife.adapters.xcomponent.pydantic_form.widget_factory.base import (
11
+ BaseWidgetBuilder,
12
+ )
13
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.base import Widget
14
+ from fastlife.adapters.xcomponent.pydantic_form.widgets.union import UnionWidget
15
+ from fastlife.shared_utils.infer import is_complex_type, is_union
16
+
17
+
18
+ class UnionBuilder(BaseWidgetBuilder[Any]):
19
+ """Builder for Union."""
20
+
21
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
22
+ """True for unions Union[A,B], A | B or event Optional[A], A | None"""
23
+ return is_union(typ)
24
+
25
+ def build(
26
+ self,
27
+ *,
28
+ field_name: str,
29
+ field_type: type[Any],
30
+ field: FieldInfo | None,
31
+ value: Any | None,
32
+ form_errors: Mapping[str, Any],
33
+ removable: bool,
34
+ ) -> Widget[Any]:
35
+ """Build the widget."""
36
+ types: list[type[Any]] = []
37
+ # required = True
38
+ for typ in field_type.__args__: # type: ignore
39
+ if typ is NoneType:
40
+ # required = False
41
+ continue
42
+ types.append(typ) # type: ignore
43
+
44
+ if (
45
+ not removable
46
+ and len(types) == 1
47
+ # if the optional type is a complex type,
48
+ and not is_complex_type(types[0])
49
+ ):
50
+ return self.factory.build( # coverage: ignore
51
+ types[0],
52
+ name=field_name,
53
+ field=field,
54
+ value=value,
55
+ form_errors=form_errors,
56
+ removable=False,
57
+ )
58
+ child = None
59
+ if value:
60
+ for typ in types:
61
+ try:
62
+ typ(**value)
63
+ except ValidationError:
64
+ pass
65
+ else:
66
+ child = self.factory.build(
67
+ typ,
68
+ name=field_name,
69
+ field=field,
70
+ value=value,
71
+ form_errors=form_errors,
72
+ removable=False,
73
+ )
74
+
75
+ widget = UnionWidget[Any](
76
+ name=field_name,
77
+ # we assume those types are BaseModel
78
+ value=child,
79
+ children_types=types, # type: ignore
80
+ title=field.title or "" if field else "",
81
+ hint=field.description if field else None,
82
+ aria_label=(
83
+ field.json_schema_extra.get("aria_label") # type:ignore
84
+ if field and field.json_schema_extra
85
+ else None
86
+ ),
87
+ token=self.factory.token,
88
+ removable=removable,
89
+ error=form_errors.get(field_name),
90
+ )
91
+
92
+ return widget