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.
- fastlife/adapters/jinjax/renderer.py +49 -25
- fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
- fastlife/adapters/jinjax/widget_factory/base.py +38 -0
- fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
- fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
- fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
- fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
- fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
- fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
- fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
- fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
- fastlife/adapters/jinjax/widgets/base.py +6 -4
- fastlife/adapters/jinjax/widgets/checklist.py +1 -1
- fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
- fastlife/adapters/jinjax/widgets/hidden.py +2 -0
- fastlife/adapters/jinjax/widgets/model.py +4 -1
- fastlife/adapters/jinjax/widgets/sequence.py +3 -2
- fastlife/adapters/jinjax/widgets/text.py +9 -10
- fastlife/adapters/jinjax/widgets/union.py +9 -7
- fastlife/components/Form.jinja +12 -0
- fastlife/config/configurator.py +23 -24
- fastlife/config/exceptions.py +4 -1
- fastlife/config/openapiextra.py +1 -0
- fastlife/config/resources.py +26 -27
- fastlife/config/settings.py +2 -0
- fastlife/config/views.py +3 -1
- fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
- fastlife/middlewares/session/middleware.py +2 -2
- fastlife/middlewares/session/serializer.py +6 -5
- fastlife/request/form.py +7 -6
- fastlife/request/form_data.py +2 -6
- fastlife/routing/route.py +3 -1
- fastlife/routing/router.py +1 -0
- fastlife/security/csrf.py +2 -1
- fastlife/security/policy.py +2 -1
- fastlife/services/locale_negociator.py +2 -1
- fastlife/services/policy.py +3 -2
- fastlife/services/templates.py +2 -1
- fastlife/services/translations.py +15 -8
- fastlife/shared_utils/infer.py +4 -3
- fastlife/shared_utils/resolver.py +64 -4
- fastlife/templates/binding.py +2 -1
- fastlife/testing/__init__.py +1 -0
- fastlife/testing/dom.py +140 -0
- fastlife/testing/form.py +204 -0
- fastlife/testing/session.py +67 -0
- fastlife/testing/testclient.py +7 -390
- fastlife/views/pydantic_form.py +4 -4
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
- fastlife/adapters/jinjax/widgets/factory.py +0 -525
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
- {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.
|
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
|
-
|
256
|
-
|
257
|
-
|
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:
|
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:
|
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:
|
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
|
+
)
|