fastlifeweb 0.16.3__py3-none-any.whl → 0.17.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. fastlife/adapters/jinjax/renderer.py +49 -25
  2. fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
  3. fastlife/adapters/jinjax/widget_factory/base.py +38 -0
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
  7. fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
  15. fastlife/adapters/jinjax/widgets/base.py +6 -4
  16. fastlife/adapters/jinjax/widgets/checklist.py +1 -1
  17. fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
  18. fastlife/adapters/jinjax/widgets/hidden.py +2 -0
  19. fastlife/adapters/jinjax/widgets/model.py +4 -1
  20. fastlife/adapters/jinjax/widgets/sequence.py +3 -2
  21. fastlife/adapters/jinjax/widgets/text.py +9 -10
  22. fastlife/adapters/jinjax/widgets/union.py +9 -7
  23. fastlife/components/Form.jinja +12 -0
  24. fastlife/config/configurator.py +23 -24
  25. fastlife/config/exceptions.py +4 -1
  26. fastlife/config/openapiextra.py +1 -0
  27. fastlife/config/resources.py +26 -27
  28. fastlife/config/settings.py +2 -0
  29. fastlife/config/views.py +3 -1
  30. fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
  31. fastlife/middlewares/session/middleware.py +2 -2
  32. fastlife/middlewares/session/serializer.py +6 -5
  33. fastlife/request/form.py +7 -6
  34. fastlife/request/form_data.py +2 -6
  35. fastlife/routing/route.py +3 -1
  36. fastlife/routing/router.py +1 -0
  37. fastlife/security/csrf.py +2 -1
  38. fastlife/security/policy.py +2 -1
  39. fastlife/services/locale_negociator.py +2 -1
  40. fastlife/services/policy.py +3 -2
  41. fastlife/services/templates.py +2 -1
  42. fastlife/services/translations.py +15 -8
  43. fastlife/shared_utils/infer.py +4 -3
  44. fastlife/shared_utils/resolver.py +64 -4
  45. fastlife/templates/binding.py +2 -1
  46. fastlife/testing/__init__.py +1 -0
  47. fastlife/testing/dom.py +140 -0
  48. fastlife/testing/form.py +204 -0
  49. fastlife/testing/session.py +67 -0
  50. fastlife/testing/testclient.py +7 -390
  51. fastlife/views/pydantic_form.py +4 -4
  52. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
  53. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
  54. fastlife/adapters/jinjax/widgets/factory.py +0 -525
  55. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
  56. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
@@ -6,16 +6,11 @@ import ast
6
6
  import logging
7
7
  import re
8
8
  import textwrap
9
+ from collections.abc import Iterator, Mapping, MutableMapping, Sequence
9
10
  from pathlib import Path
10
11
  from typing import (
11
12
  TYPE_CHECKING,
12
13
  Any,
13
- Iterator,
14
- Mapping,
15
- MutableMapping,
16
- Optional,
17
- Sequence,
18
- Type,
19
14
  cast,
20
15
  )
21
16
 
@@ -29,7 +24,7 @@ from markupsafe import Markup
29
24
  from pydantic.fields import FieldInfo
30
25
 
31
26
  from fastlife import Request
32
- from fastlife.adapters.jinjax.widgets.factory import WidgetFactory
27
+ from fastlife.adapters.jinjax.widget_factory.factory import WidgetFactory
33
28
  from fastlife.request.form import FormModel
34
29
  from fastlife.request.localizer import get_localizer
35
30
 
@@ -108,7 +103,7 @@ def generate_docstring(
108
103
  kwonlyargs = func_def.args.kwonlyargs
109
104
  kw_defaults = func_def.args.kw_defaults
110
105
 
111
- for arg, default in zip(kwonlyargs, kw_defaults):
106
+ for arg, default in zip(kwonlyargs, kw_defaults, strict=False):
112
107
  process_arg(arg, default)
113
108
 
114
109
  if add_content:
@@ -226,6 +221,20 @@ def component():
226
221
  return ret
227
222
 
228
223
 
224
+ def to_include(
225
+ name: str,
226
+ ignores: Sequence[re.Pattern[str]] | None = None,
227
+ includes: Sequence[re.Pattern[str]] | None = None,
228
+ ) -> bool:
229
+ if includes and not any(include.match(name) for include in includes):
230
+ return False
231
+
232
+ if ignores and any(ignore.match(name) for ignore in ignores):
233
+ return False
234
+
235
+ return True
236
+
237
+
229
238
  class InspectableCatalog(Catalog):
230
239
  """
231
240
  JinjaX Catalog with introspection support.
@@ -252,20 +261,9 @@ class InspectableCatalog(Catalog):
252
261
  prefix, name, file_ext=file_ext
253
262
  )
254
263
 
255
- to_include = True
256
- if includes:
257
- to_include = False
258
- for include in includes:
259
- if include.match(name):
260
- to_include = True
261
- break
262
- if to_include and ignores:
263
- for ignore in ignores:
264
- if ignore.match(name):
265
- to_include = False
266
- break
267
-
268
- if to_include:
264
+ is_included = to_include(name, ignores, includes)
265
+
266
+ if is_included:
269
267
  component = InspectableComponent(
270
268
  name=name, prefix=prefix, path=path, source=path.read_text()
271
269
  )
@@ -342,9 +340,17 @@ class JinjaxRenderer(AbstractTemplateRenderer):
342
340
  self,
343
341
  template: str,
344
342
  *,
345
- globals: Optional[Mapping[str, Any]] = None,
343
+ globals: Mapping[str, Any] | None = None,
346
344
  **params: Any,
347
345
  ) -> str:
346
+ """
347
+ Render the JinjaX component with the given parameter.
348
+
349
+ :param template: the template to render
350
+ :param globals: parameters that will be used by the JinjaX component and all its
351
+ child components without "props drilling".
352
+ :param params: parameters used to render the template.
353
+ """
348
354
  # Jinja template does accept the file extention while rendering the template
349
355
  # we strip it before rendering.
350
356
  template = template[: -len(self.settings.jinjax_file_ext) - 1]
@@ -360,19 +366,37 @@ class JinjaxRenderer(AbstractTemplateRenderer):
360
366
  )
361
367
 
362
368
  def pydantic_form(
363
- self, model: FormModel[Any], *, token: Optional[str] = None
369
+ self, model: FormModel[Any], *, token: str | None = None
364
370
  ) -> Markup:
371
+ """
372
+ Generate HTML markup to build a form from the given form model.
373
+
374
+ :param model: the form model that will be transformed to markup.
375
+ :param token: a token used to ensure that unique identifier are unique.
376
+ :return: HTML Markup generated by composign fields widgets.
377
+ """
365
378
  return WidgetFactory(self, token).get_markup(model)
366
379
 
367
380
  def pydantic_form_field(
368
381
  self,
369
- model: Type[Any],
382
+ model: type[Any],
370
383
  *,
371
384
  name: str | None,
372
385
  token: str | None,
373
386
  removable: bool,
374
387
  field: FieldInfo | None,
375
388
  ) -> Markup:
389
+ """
390
+ Generate HTML for a particular field in a form.
391
+
392
+ This function is used to generate union subtypes in Ajax requests.
393
+ :param model: a pydantic or python builtin type that is requests to be rendered
394
+ :param name: name for the field
395
+ :param token: the token of the form to render unique identifiers
396
+ :param removable: add a way let the user remove the widget
397
+ :param field: only render this particular field for the model.
398
+ :return: HTML Markup.
399
+ """
376
400
  return (
377
401
  WidgetFactory(self, token)
378
402
  .get_widget(
@@ -0,0 +1 @@
1
+ """Factory for widgets in order to edit pydantic model."""
@@ -0,0 +1,38 @@
1
+ """Abstract class for builder."""
2
+
3
+ import abc
4
+ from collections.abc import Mapping
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ from pydantic.fields import FieldInfo
8
+
9
+ from fastlife.adapters.jinjax.widgets.base import Widget
10
+
11
+ if TYPE_CHECKING:
12
+ from fastlife.adapters.jinjax.widget_factory.factory import WidgetFactory
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ class BaseWidgetBuilder(abc.ABC, Generic[T]):
18
+ """Base class for the builder of widget."""
19
+
20
+ def __init__(self, factory: "WidgetFactory") -> None:
21
+ self.factory = factory
22
+
23
+ @abc.abstractmethod
24
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
25
+ """Return true if the builder accept to build a widget for this type."""
26
+
27
+ @abc.abstractmethod
28
+ def build(
29
+ self,
30
+ *,
31
+ field_name: str,
32
+ field_type: type[Any],
33
+ field: FieldInfo | None,
34
+ value: T | None,
35
+ form_errors: Mapping[str, Any],
36
+ removable: bool,
37
+ ) -> Widget[T]:
38
+ """Build the widget"""
@@ -0,0 +1,43 @@
1
+ """Handle boolean values."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
9
+ from fastlife.adapters.jinjax.widgets.boolean import BooleanWidget
10
+
11
+
12
+ class BoolBuilder(BaseWidgetBuilder[bool]):
13
+ """Builder for boolean."""
14
+
15
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
16
+ """True for boolean."""
17
+ return issubclass(typ, bool)
18
+
19
+ def build(
20
+ self,
21
+ *,
22
+ field_name: str,
23
+ field_type: type[Any],
24
+ field: FieldInfo | None,
25
+ value: bool | None,
26
+ form_errors: Mapping[str, Any],
27
+ removable: bool,
28
+ ) -> BooleanWidget:
29
+ """Build the widget."""
30
+ return BooleanWidget(
31
+ field_name,
32
+ removable=removable,
33
+ title=field.title if field else "",
34
+ hint=field.description if field else None,
35
+ aria_label=(
36
+ field.json_schema_extra.get("aria_label") # type:ignore
37
+ if field and field.json_schema_extra
38
+ else None
39
+ ),
40
+ token=self.factory.token,
41
+ value=value or False,
42
+ error=form_errors.get(field_name),
43
+ )
@@ -0,0 +1,46 @@
1
+ """Handle EmailStr pydantic type."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from pydantic.fields import FieldInfo
7
+ from pydantic.networks import EmailStr
8
+
9
+ from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
10
+ from fastlife.adapters.jinjax.widgets.text import TextWidget
11
+
12
+
13
+ class EmailStrBuilder(BaseWidgetBuilder[EmailStr]):
14
+ """Builder for Pydantic EmailStr."""
15
+
16
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
17
+ """True for EmailStr."""
18
+ return issubclass(typ, EmailStr) # type: ignore
19
+
20
+ def build(
21
+ self,
22
+ *,
23
+ field_name: str,
24
+ field_type: type[Any],
25
+ field: FieldInfo | None,
26
+ value: EmailStr | None,
27
+ form_errors: Mapping[str, Any],
28
+ removable: bool,
29
+ ) -> TextWidget:
30
+ """Build the widget."""
31
+ return TextWidget(
32
+ field_name,
33
+ input_type="email",
34
+ placeholder=str(field.examples[0]) if field and field.examples else None,
35
+ removable=removable,
36
+ title=field.title if field else "",
37
+ hint=field.description if field else None,
38
+ aria_label=(
39
+ field.json_schema_extra.get("aria_label") # type:ignore
40
+ if field and field.json_schema_extra
41
+ else None
42
+ ),
43
+ token=self.factory.token,
44
+ value=str(value),
45
+ error=form_errors.get(field_name),
46
+ )
@@ -0,0 +1,47 @@
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.jinjax.widget_factory.base import BaseWidgetBuilder
10
+ from fastlife.adapters.jinjax.widgets.base import Widget
11
+ from fastlife.adapters.jinjax.widgets.dropdown import DropDownWidget
12
+
13
+
14
+ class EnumBuilder(BaseWidgetBuilder[Enum]):
15
+ """Builder for Enum."""
16
+
17
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
18
+ """True for Enum."""
19
+ return issubclass(typ, Enum)
20
+
21
+ def build(
22
+ self,
23
+ *,
24
+ field_name: str,
25
+ field_type: type[Any], # an enum subclass
26
+ field: FieldInfo | None,
27
+ value: Enum | None, # str | int | float,
28
+ form_errors: Mapping[str, Any],
29
+ removable: bool,
30
+ ) -> Widget[Enum]:
31
+ """Build the widget."""
32
+ options = [(item.name, item.value) for item in field_type] # type: ignore
33
+ return DropDownWidget(
34
+ field_name,
35
+ options=options, # type: ignore
36
+ removable=removable,
37
+ title=field.title 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
+ token=self.factory.token,
45
+ value=str(value),
46
+ error=form_errors.get(field_name),
47
+ )
@@ -0,0 +1,165 @@
1
+ """
2
+ Create markup for pydantic forms.
3
+ """
4
+
5
+ import secrets
6
+ from collections.abc import Mapping
7
+ from inspect import isclass
8
+ from typing import Any, cast, get_origin
9
+
10
+ from markupsafe import Markup
11
+ from pydantic.fields import FieldInfo
12
+
13
+ from fastlife.adapters.jinjax.widgets.base import Widget
14
+ from fastlife.request.form import FormModel
15
+ from fastlife.services.templates import AbstractTemplateRenderer
16
+
17
+ from .base import BaseWidgetBuilder
18
+ from .bool_builder import BoolBuilder
19
+ from .emailstr_builder import EmailStrBuilder
20
+ from .enum_builder import EnumBuilder
21
+ from .literal_builder import LiteralBuilder
22
+ from .model_builder import ModelBuilder
23
+ from .secretstr_builder import SecretStrBuilder
24
+ from .sequence_builder import SequenceBuilder
25
+ from .set_builder import SetBuilder
26
+ from .simpletype_builder import SimpleTypeBuilder
27
+ from .union_builder import UnionBuilder
28
+
29
+
30
+ class WidgetFactory:
31
+ """
32
+ Form builder for pydantic model.
33
+
34
+ :param renderer: template engine to render widget.
35
+ :param token: reuse a token.
36
+ """
37
+
38
+ def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
39
+ self.renderer = renderer
40
+ self.token = token or secrets.token_urlsafe(4).replace("_", "-")
41
+ self.builders: list[BaseWidgetBuilder[Any]] = [
42
+ # Order is super important here
43
+ # starts by the union type
44
+ UnionBuilder(self),
45
+ # and to other types that have an origin
46
+ SetBuilder(self),
47
+ LiteralBuilder(self),
48
+ SequenceBuilder(self),
49
+ # from this part, order does not really matter
50
+ ModelBuilder(self),
51
+ BoolBuilder(self),
52
+ EnumBuilder(self),
53
+ EmailStrBuilder(self),
54
+ SecretStrBuilder(self),
55
+ # we keep simple types, str, int at the end
56
+ SimpleTypeBuilder(self),
57
+ ]
58
+
59
+ def get_markup(
60
+ self,
61
+ model: FormModel[Any],
62
+ *,
63
+ removable: bool = False,
64
+ field: FieldInfo | None = None,
65
+ ) -> Markup:
66
+ """
67
+ Get the markup for the given model.
68
+
69
+ :param model: the model to build the html markup.
70
+ :param removable: Include a button to remove the model in the markup.
71
+ :param field: only build the markup of this field is not None.
72
+ """
73
+ return self.get_widget(
74
+ model.model.__class__,
75
+ model.form_data,
76
+ model.errors,
77
+ prefix=model.prefix,
78
+ removable=removable,
79
+ field=field,
80
+ ).to_html(self.renderer)
81
+
82
+ def get_widget(
83
+ self,
84
+ base: type[Any],
85
+ form_data: Mapping[str, Any],
86
+ form_errors: Mapping[str, Any],
87
+ *,
88
+ prefix: str,
89
+ removable: bool,
90
+ field: FieldInfo | None = None,
91
+ ) -> Widget[Any]:
92
+ """
93
+ build the widget for the given type and return it.
94
+ :param base: the type to build, it has to be a builtin or a Pydantic model.
95
+ :param form_data: form values to render.
96
+ :param form_errors: form errors to render.
97
+ """
98
+ return self.build(
99
+ base,
100
+ value=form_data.get(prefix, {}),
101
+ form_errors=form_errors,
102
+ name=prefix,
103
+ removable=removable,
104
+ field=field,
105
+ )
106
+
107
+ def build(
108
+ self,
109
+ typ: type[Any],
110
+ *,
111
+ name: str = "",
112
+ value: Any,
113
+ removable: bool,
114
+ form_errors: Mapping[str, Any],
115
+ field: FieldInfo | None = None,
116
+ ) -> Widget[Any]:
117
+ """
118
+ build widget tree for the given type.
119
+ This function is recurive and shoud not be used directly.
120
+ The type is a composite, it can be pydantic model, builtin, list or unions.
121
+
122
+ The {meth}`WidgetFactory.get_widget` or {meth}`WidgetFactory.get_markup`
123
+ should be used.
124
+
125
+ :param typ: the type to build, it has to be a builtin or a Pydantic model.
126
+ :param name: name of the widget to build.
127
+ :param value: value for the widget.
128
+ :param removable: True if it has to include a remove button.
129
+ :param form_errors: errors in the form.
130
+ :param field: field information used to customize the widget.
131
+ """
132
+ if field and field.metadata:
133
+ for widget in field.metadata:
134
+ if isclass(widget) and issubclass(widget, Widget):
135
+ return cast(
136
+ Widget[Any],
137
+ widget(
138
+ name,
139
+ value=value,
140
+ removable=removable,
141
+ title=field.title if field else "",
142
+ hint=field.description if field else None,
143
+ aria_label=(
144
+ field.json_schema_extra.get("aria_label") # type:ignore
145
+ if field and field.json_schema_extra
146
+ else None
147
+ ),
148
+ token=self.token,
149
+ error=form_errors.get(name),
150
+ ),
151
+ )
152
+
153
+ type_origin = get_origin(typ)
154
+ for builder in self.builders:
155
+ if builder.accept(typ, type_origin):
156
+ return builder.build(
157
+ field_name=name,
158
+ field_type=typ,
159
+ field=field,
160
+ value=value,
161
+ form_errors=form_errors,
162
+ removable=removable,
163
+ )
164
+
165
+ raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
@@ -0,0 +1,52 @@
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.jinjax.widget_factory.base import BaseWidgetBuilder
9
+ from fastlife.adapters.jinjax.widgets.dropdown import DropDownWidget
10
+ from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
11
+
12
+
13
+ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
14
+ """Builder for Literal."""
15
+
16
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
17
+ """True for Literal."""
18
+ return origin is Literal
19
+
20
+ def build(
21
+ self,
22
+ *,
23
+ field_name: str,
24
+ field_type: type[Any], # a literal actually
25
+ field: FieldInfo | None,
26
+ value: str | None,
27
+ form_errors: Mapping[str, Any],
28
+ removable: bool,
29
+ ) -> HiddenWidget | DropDownWidget:
30
+ """Build the widget."""
31
+ choices: list[str] = field_type.__args__ # type: ignore
32
+ if len(choices) == 1:
33
+ return HiddenWidget(
34
+ field_name,
35
+ value=choices[0],
36
+ token=self.factory.token,
37
+ )
38
+ return DropDownWidget(
39
+ field_name,
40
+ options=choices,
41
+ removable=removable,
42
+ title=field.title if field else "",
43
+ hint=field.description if field else None,
44
+ aria_label=(
45
+ field.json_schema_extra.get("aria_label") # type:ignore
46
+ if field and field.json_schema_extra
47
+ else None
48
+ ),
49
+ token=self.factory.token,
50
+ value=str(value),
51
+ error=form_errors.get(field_name),
52
+ )
@@ -0,0 +1,64 @@
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.jinjax.widget_factory.base import BaseWidgetBuilder
10
+ from fastlife.adapters.jinjax.widgets.base import Widget
11
+ from fastlife.adapters.jinjax.widgets.model import ModelWidget
12
+
13
+
14
+ class ModelBuilder(BaseWidgetBuilder[Mapping[str, Any]]):
15
+ """Builder for Pydantic BaseModel values."""
16
+
17
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
18
+ """True for Pydantic BaseModel."""
19
+ return issubclass(typ, BaseModel)
20
+
21
+ def build(
22
+ self,
23
+ *,
24
+ field_name: str,
25
+ field_type: type[BaseModel],
26
+ field: FieldInfo | None,
27
+ value: Mapping[str, Any] | None,
28
+ form_errors: Mapping[str, Any],
29
+ removable: bool,
30
+ ) -> Widget[Any]:
31
+ """Build the widget."""
32
+ value = value or {}
33
+ ret: dict[str, Any] = {}
34
+ for key, child_field in field_type.model_fields.items():
35
+ child_key = f"{field_name}.{key}" if field_name else key
36
+ if child_field.exclude:
37
+ continue
38
+ if child_field.annotation is None:
39
+ raise ValueError( # coverage: ignore
40
+ f"Missing annotation for {child_field} in {child_key}"
41
+ )
42
+ ret[key] = self.factory.build(
43
+ child_field.annotation,
44
+ name=child_key,
45
+ field=child_field,
46
+ value=value.get(key),
47
+ form_errors=form_errors,
48
+ removable=False,
49
+ )
50
+ return ModelWidget(
51
+ field_name,
52
+ value=list(ret.values()),
53
+ removable=removable,
54
+ title=field.title if field and field.title else "",
55
+ hint=field.description if field else None,
56
+ aria_label=(
57
+ field.json_schema_extra.get("aria_label") # type:ignore
58
+ if field and field.json_schema_extra
59
+ else None
60
+ ),
61
+ token=self.factory.token,
62
+ error=form_errors.get(field_name),
63
+ nested=field is not None,
64
+ )
@@ -0,0 +1,47 @@
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.jinjax.widget_factory.base import BaseWidgetBuilder
10
+ from fastlife.adapters.jinjax.widgets.base import Widget
11
+ from fastlife.adapters.jinjax.widgets.text import TextWidget
12
+
13
+
14
+ class SecretStrBuilder(BaseWidgetBuilder[SecretStr]):
15
+ """Builder for Pydantic SecretStr."""
16
+
17
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
18
+ """True for Pydantic SecretStr."""
19
+ return issubclass(typ, SecretStr)
20
+
21
+ def build(
22
+ self,
23
+ *,
24
+ field_name: str,
25
+ field_type: type[Any],
26
+ field: FieldInfo | None,
27
+ value: SecretStr | None,
28
+ form_errors: Mapping[str, Any],
29
+ removable: bool,
30
+ ) -> Widget[SecretStr]:
31
+ """Build the widget."""
32
+ return TextWidget(
33
+ field_name,
34
+ input_type="password",
35
+ placeholder=str(field.examples[0]) if field and field.examples else None,
36
+ removable=removable,
37
+ title=field.title 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
+ token=self.factory.token,
45
+ value=value.get_secret_value() if value else "",
46
+ error=form_errors.get(field_name),
47
+ )