fastlifeweb 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastlife/__init__.py +3 -2
- fastlife/request/form_data.py +7 -22
- fastlife/request/model_result.py +91 -0
- fastlife/shared_utils/infer.py +1 -1
- fastlife/templates/pydantic_form/Boolean.jinja +7 -2
- fastlife/templates/pydantic_form/Checklist.jinja +3 -1
- fastlife/templates/pydantic_form/Dropdown.jinja +1 -0
- fastlife/templates/pydantic_form/Error.jinja +4 -0
- fastlife/templates/pydantic_form/Model.jinja +1 -0
- fastlife/templates/pydantic_form/Sequence.jinja +1 -0
- fastlife/templates/pydantic_form/Text.jinja +1 -0
- fastlife/templates/pydantic_form/Union.jinja +4 -4
- fastlife/templating/renderer/abstract.py +16 -2
- fastlife/templating/renderer/jinjax.py +25 -4
- fastlife/templating/renderer/widgets/base.py +7 -5
- fastlife/templating/renderer/widgets/boolean.py +7 -1
- fastlife/templating/renderer/widgets/checklist.py +13 -2
- fastlife/templating/renderer/widgets/dropdown.py +7 -1
- fastlife/templating/renderer/widgets/factory.py +69 -16
- fastlife/templating/renderer/widgets/model.py +7 -1
- fastlife/templating/renderer/widgets/sequence.py +7 -1
- fastlife/templating/renderer/widgets/text.py +3 -1
- fastlife/templating/renderer/widgets/union.py +7 -1
- fastlife/testing/testclient.py +102 -0
- fastlife/views/pydantic_form.py +6 -2
- {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/METADATA +2 -2
- {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/RECORD +29 -27
- {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.5.1.dist-info → fastlifeweb-0.6.1.dist-info}/WHEEL +0 -0
@@ -2,6 +2,7 @@ import secrets
|
|
2
2
|
from collections.abc import MutableSequence, Sequence
|
3
3
|
from decimal import Decimal
|
4
4
|
from enum import Enum
|
5
|
+
from inspect import isclass
|
5
6
|
from types import NoneType
|
6
7
|
from typing import Any, Literal, Mapping, Optional, Type, cast, get_origin
|
7
8
|
from uuid import UUID
|
@@ -10,6 +11,7 @@ from markupsafe import Markup
|
|
10
11
|
from pydantic import BaseModel, EmailStr, SecretStr, ValidationError
|
11
12
|
from pydantic.fields import FieldInfo
|
12
13
|
|
14
|
+
from fastlife.request.model_result import ModelResult
|
13
15
|
from fastlife.shared_utils.infer import is_complex_type, is_union
|
14
16
|
from fastlife.templating.renderer.abstract import AbstractTemplateRenderer
|
15
17
|
from fastlife.templating.renderer.widgets.boolean import BooleanWidget
|
@@ -31,21 +33,25 @@ class WidgetFactory:
|
|
31
33
|
|
32
34
|
def get_markup(
|
33
35
|
self,
|
34
|
-
|
35
|
-
form_data: Mapping[str, Any],
|
36
|
+
model: ModelResult[Any],
|
36
37
|
*,
|
37
|
-
prefix: str,
|
38
38
|
removable: bool,
|
39
39
|
field: FieldInfo | None = None,
|
40
40
|
) -> Markup:
|
41
41
|
return self.get_widget(
|
42
|
-
|
42
|
+
model.model.__class__,
|
43
|
+
model.form_data,
|
44
|
+
model.errors,
|
45
|
+
prefix=model.prefix,
|
46
|
+
removable=removable,
|
47
|
+
field=field,
|
43
48
|
).to_html(self.renderer)
|
44
49
|
|
45
50
|
def get_widget(
|
46
51
|
self,
|
47
52
|
base: Type[Any],
|
48
53
|
form_data: Mapping[str, Any],
|
54
|
+
form_errors: Mapping[str, Any],
|
49
55
|
*,
|
50
56
|
prefix: str,
|
51
57
|
removable: bool,
|
@@ -54,6 +60,7 @@ class WidgetFactory:
|
|
54
60
|
return self.build(
|
55
61
|
base,
|
56
62
|
value=form_data.get(prefix, {}),
|
63
|
+
form_errors=form_errors,
|
57
64
|
name=prefix,
|
58
65
|
removable=removable,
|
59
66
|
field=field,
|
@@ -66,11 +73,12 @@ class WidgetFactory:
|
|
66
73
|
name: str = "",
|
67
74
|
value: Any,
|
68
75
|
removable: bool,
|
76
|
+
form_errors: Mapping[str, Any],
|
69
77
|
field: FieldInfo | None = None,
|
70
78
|
) -> Widget[Any]:
|
71
79
|
if field and field.metadata:
|
72
80
|
for widget in field.metadata:
|
73
|
-
if issubclass(widget, Widget):
|
81
|
+
if isclass(widget) and issubclass(widget, Widget):
|
74
82
|
return cast(
|
75
83
|
Widget[Any],
|
76
84
|
widget(
|
@@ -80,44 +88,59 @@ class WidgetFactory:
|
|
80
88
|
title=field.title if field else "",
|
81
89
|
aria_label=field.description if field else None,
|
82
90
|
token=self.token,
|
91
|
+
error=form_errors.get(name),
|
83
92
|
),
|
84
93
|
)
|
85
94
|
|
86
95
|
type_origin = get_origin(typ)
|
87
96
|
if type_origin:
|
88
97
|
if is_union(typ):
|
89
|
-
return self.build_union(name, typ, field, value, removable)
|
98
|
+
return self.build_union(name, typ, field, value, form_errors, removable)
|
90
99
|
|
91
100
|
if (
|
92
101
|
type_origin is Sequence
|
93
102
|
or type_origin is MutableSequence
|
94
103
|
or type_origin is list
|
95
104
|
):
|
96
|
-
return self.build_sequence(
|
105
|
+
return self.build_sequence(
|
106
|
+
name, typ, field, value, form_errors, removable
|
107
|
+
)
|
97
108
|
|
98
109
|
if type_origin is Literal:
|
99
|
-
return self.build_literal(
|
110
|
+
return self.build_literal(
|
111
|
+
name, typ, field, value, form_errors, removable
|
112
|
+
)
|
100
113
|
|
101
114
|
if type_origin is set:
|
102
|
-
return self.build_set(name, typ, field, value, removable)
|
115
|
+
return self.build_set(name, typ, field, value, form_errors, removable)
|
103
116
|
|
104
117
|
if issubclass(typ, Enum): # if it raises here, the type_origin is unknown
|
105
|
-
return self.build_enum(name, typ, field, value, removable)
|
118
|
+
return self.build_enum(name, typ, field, value, form_errors, removable)
|
106
119
|
|
107
120
|
if issubclass(typ, BaseModel): # if it raises here, the type_origin is unknown
|
108
|
-
return self.build_model(
|
121
|
+
return self.build_model(
|
122
|
+
name, typ, field, value or {}, form_errors, removable
|
123
|
+
)
|
109
124
|
|
110
125
|
if issubclass(typ, bool):
|
111
|
-
return self.build_boolean(
|
126
|
+
return self.build_boolean(
|
127
|
+
name, typ, field, value or False, form_errors, removable
|
128
|
+
)
|
112
129
|
|
113
130
|
if issubclass(typ, EmailStr): # type: ignore
|
114
|
-
return self.build_emailtype(
|
131
|
+
return self.build_emailtype(
|
132
|
+
name, typ, field, value or "", form_errors, removable
|
133
|
+
)
|
115
134
|
|
116
135
|
if issubclass(typ, SecretStr):
|
117
|
-
return self.build_secretstr(
|
136
|
+
return self.build_secretstr(
|
137
|
+
name, typ, field, value or "", form_errors, removable
|
138
|
+
)
|
118
139
|
|
119
140
|
if issubclass(typ, (int, str, float, Decimal, UUID)):
|
120
|
-
return self.build_simpletype(
|
141
|
+
return self.build_simpletype(
|
142
|
+
name, typ, field, value or "", form_errors, removable
|
143
|
+
)
|
121
144
|
|
122
145
|
raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
|
123
146
|
|
@@ -127,6 +150,7 @@ class WidgetFactory:
|
|
127
150
|
typ: Type[BaseModel],
|
128
151
|
field: Optional[FieldInfo],
|
129
152
|
value: Mapping[str, Any],
|
153
|
+
form_errors: Mapping[str, Any],
|
130
154
|
removable: bool,
|
131
155
|
) -> Widget[Any]:
|
132
156
|
ret: dict[str, Any] = {}
|
@@ -143,6 +167,7 @@ class WidgetFactory:
|
|
143
167
|
name=child_key,
|
144
168
|
field=field,
|
145
169
|
value=value.get(key),
|
170
|
+
form_errors=form_errors,
|
146
171
|
removable=False,
|
147
172
|
)
|
148
173
|
return ModelWidget(
|
@@ -151,6 +176,7 @@ class WidgetFactory:
|
|
151
176
|
removable=removable,
|
152
177
|
title=get_title(typ),
|
153
178
|
token=self.token,
|
179
|
+
error=form_errors.get(field_name),
|
154
180
|
)
|
155
181
|
|
156
182
|
def build_union(
|
@@ -159,6 +185,7 @@ class WidgetFactory:
|
|
159
185
|
field_type: Type[Any],
|
160
186
|
field: Optional[FieldInfo],
|
161
187
|
value: Any,
|
188
|
+
form_errors: Mapping[str, Any],
|
162
189
|
removable: bool,
|
163
190
|
) -> Widget[Any]:
|
164
191
|
types: list[Type[Any]] = []
|
@@ -176,7 +203,12 @@ class WidgetFactory:
|
|
176
203
|
and not is_complex_type(types[0])
|
177
204
|
):
|
178
205
|
return self.build(
|
179
|
-
types[0],
|
206
|
+
types[0],
|
207
|
+
name=field_name,
|
208
|
+
field=field,
|
209
|
+
value=value,
|
210
|
+
form_errors=form_errors,
|
211
|
+
removable=False,
|
180
212
|
)
|
181
213
|
child = None
|
182
214
|
if value:
|
@@ -191,6 +223,7 @@ class WidgetFactory:
|
|
191
223
|
name=field_name,
|
192
224
|
field=field,
|
193
225
|
value=value,
|
226
|
+
form_errors=form_errors,
|
194
227
|
removable=False,
|
195
228
|
)
|
196
229
|
|
@@ -202,6 +235,7 @@ class WidgetFactory:
|
|
202
235
|
title=field.title if field else "",
|
203
236
|
token=self.token,
|
204
237
|
removable=removable,
|
238
|
+
error=form_errors.get(field_name),
|
205
239
|
)
|
206
240
|
|
207
241
|
return widget
|
@@ -212,6 +246,7 @@ class WidgetFactory:
|
|
212
246
|
field_type: Type[Any],
|
213
247
|
field: Optional[FieldInfo],
|
214
248
|
value: Optional[Sequence[Any]],
|
249
|
+
form_errors: Mapping[str, Any],
|
215
250
|
removable: bool,
|
216
251
|
) -> Widget[Any]:
|
217
252
|
typ = field_type.__args__[0] # type: ignore
|
@@ -222,6 +257,7 @@ class WidgetFactory:
|
|
222
257
|
name=f"{field_name}.{idx}",
|
223
258
|
value=v,
|
224
259
|
field=field,
|
260
|
+
form_errors=form_errors,
|
225
261
|
removable=True,
|
226
262
|
)
|
227
263
|
for idx, v in enumerate(value)
|
@@ -234,6 +270,7 @@ class WidgetFactory:
|
|
234
270
|
item_type=typ, # type: ignore
|
235
271
|
token=self.token,
|
236
272
|
removable=removable,
|
273
|
+
error=form_errors.get(field_name),
|
237
274
|
)
|
238
275
|
|
239
276
|
def build_set(
|
@@ -242,6 +279,7 @@ class WidgetFactory:
|
|
242
279
|
field_type: Type[Any],
|
243
280
|
field: Optional[FieldInfo],
|
244
281
|
value: Optional[Sequence[Any]],
|
282
|
+
form_errors: Mapping[str, Any],
|
245
283
|
removable: bool,
|
246
284
|
) -> Widget[Any]:
|
247
285
|
choice_wrapper = field_type.__args__[0]
|
@@ -257,6 +295,7 @@ class WidgetFactory:
|
|
257
295
|
checked=c in value if value else False, # type: ignore
|
258
296
|
name=field_name,
|
259
297
|
token=self.token,
|
298
|
+
error=form_errors.get(f"{field_name}-{c}"),
|
260
299
|
)
|
261
300
|
for c in litchoice
|
262
301
|
]
|
@@ -271,6 +310,7 @@ class WidgetFactory:
|
|
271
310
|
checked=e.name in value if value else False, # type: ignore
|
272
311
|
name=field_name,
|
273
312
|
token=self.token,
|
313
|
+
error=form_errors.get(f"{field_name}-{e.name}"),
|
274
314
|
)
|
275
315
|
for e in choice_wrapper
|
276
316
|
]
|
@@ -283,6 +323,7 @@ class WidgetFactory:
|
|
283
323
|
token=self.token,
|
284
324
|
value=choices,
|
285
325
|
removable=removable,
|
326
|
+
error=form_errors.get(field_name),
|
286
327
|
)
|
287
328
|
|
288
329
|
def build_boolean(
|
@@ -291,6 +332,7 @@ class WidgetFactory:
|
|
291
332
|
field_type: Type[Any],
|
292
333
|
field: FieldInfo | None,
|
293
334
|
value: bool,
|
335
|
+
form_errors: Mapping[str, Any],
|
294
336
|
removable: bool,
|
295
337
|
) -> Widget[Any]:
|
296
338
|
return BooleanWidget(
|
@@ -299,6 +341,7 @@ class WidgetFactory:
|
|
299
341
|
title=field.title if field else "",
|
300
342
|
token=self.token,
|
301
343
|
value=value,
|
344
|
+
error=form_errors.get(field_name),
|
302
345
|
)
|
303
346
|
|
304
347
|
def build_emailtype(
|
@@ -307,6 +350,7 @@ class WidgetFactory:
|
|
307
350
|
field_type: Type[Any],
|
308
351
|
field: FieldInfo | None,
|
309
352
|
value: str | int | float,
|
353
|
+
form_errors: Mapping[str, Any],
|
310
354
|
removable: bool,
|
311
355
|
) -> Widget[Any]:
|
312
356
|
return TextWidget(
|
@@ -318,6 +362,7 @@ class WidgetFactory:
|
|
318
362
|
title=field.title if field else "",
|
319
363
|
token=self.token,
|
320
364
|
value=str(value),
|
365
|
+
error=form_errors.get(field_name),
|
321
366
|
)
|
322
367
|
|
323
368
|
def build_secretstr(
|
@@ -326,6 +371,7 @@ class WidgetFactory:
|
|
326
371
|
field_type: Type[Any],
|
327
372
|
field: FieldInfo | None,
|
328
373
|
value: SecretStr | str,
|
374
|
+
form_errors: Mapping[str, Any],
|
329
375
|
removable: bool,
|
330
376
|
) -> Widget[Any]:
|
331
377
|
return TextWidget(
|
@@ -337,6 +383,7 @@ class WidgetFactory:
|
|
337
383
|
title=field.title if field else "",
|
338
384
|
token=self.token,
|
339
385
|
value=value.get_secret_value() if isinstance(value, SecretStr) else value,
|
386
|
+
error=form_errors.get(field_name),
|
340
387
|
)
|
341
388
|
|
342
389
|
def build_literal(
|
@@ -345,6 +392,7 @@ class WidgetFactory:
|
|
345
392
|
field_type: Type[Any], # a literal actually
|
346
393
|
field: FieldInfo | None,
|
347
394
|
value: str | int | float,
|
395
|
+
form_errors: Mapping[str, Any],
|
348
396
|
removable: bool,
|
349
397
|
) -> Widget[Any]:
|
350
398
|
choices: list[str] = field_type.__args__ # type: ignore
|
@@ -361,6 +409,7 @@ class WidgetFactory:
|
|
361
409
|
title=field.title if field else "",
|
362
410
|
token=self.token,
|
363
411
|
value=str(value),
|
412
|
+
error=form_errors.get(field_name),
|
364
413
|
)
|
365
414
|
|
366
415
|
def build_enum(
|
@@ -369,6 +418,7 @@ class WidgetFactory:
|
|
369
418
|
field_type: Type[Any], # an enum subclass
|
370
419
|
field: FieldInfo | None,
|
371
420
|
value: str | int | float,
|
421
|
+
form_errors: Mapping[str, Any],
|
372
422
|
removable: bool,
|
373
423
|
) -> Widget[Any]:
|
374
424
|
options = [(item.name, item.value) for item in field_type] # type: ignore
|
@@ -379,6 +429,7 @@ class WidgetFactory:
|
|
379
429
|
title=field.title if field else "",
|
380
430
|
token=self.token,
|
381
431
|
value=str(value),
|
432
|
+
error=form_errors.get(field_name),
|
382
433
|
)
|
383
434
|
|
384
435
|
def build_simpletype(
|
@@ -387,6 +438,7 @@ class WidgetFactory:
|
|
387
438
|
field_type: Type[Any],
|
388
439
|
field: FieldInfo | None,
|
389
440
|
value: str | int | float,
|
441
|
+
form_errors: Mapping[str, Any],
|
390
442
|
removable: bool,
|
391
443
|
) -> Widget[Any]:
|
392
444
|
return TextWidget(
|
@@ -398,4 +450,5 @@ class WidgetFactory:
|
|
398
450
|
title=field.title if field else "",
|
399
451
|
token=self.token,
|
400
452
|
value=str(value),
|
453
|
+
error=form_errors.get(field_name),
|
401
454
|
)
|
@@ -13,12 +13,18 @@ class ModelWidget(Widget[Sequence[Widget[Any]]]):
|
|
13
13
|
name: str,
|
14
14
|
*,
|
15
15
|
value: Sequence[Widget[Any]],
|
16
|
+
error: str | None = None,
|
16
17
|
removable: bool,
|
17
18
|
title: str,
|
18
19
|
token: str,
|
19
20
|
):
|
20
21
|
super().__init__(
|
21
|
-
name,
|
22
|
+
name,
|
23
|
+
title=title,
|
24
|
+
value=value,
|
25
|
+
error=error,
|
26
|
+
removable=removable,
|
27
|
+
token=token,
|
22
28
|
)
|
23
29
|
|
24
30
|
def get_template(self) -> str:
|
@@ -15,12 +15,18 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
|
|
15
15
|
title: Optional[str],
|
16
16
|
hint: Optional[str],
|
17
17
|
value: Optional[Sequence[Widget[Any]]],
|
18
|
+
error: str | None = None,
|
18
19
|
item_type: Type[Any],
|
19
20
|
token: str,
|
20
21
|
removable: bool,
|
21
22
|
):
|
22
23
|
super().__init__(
|
23
|
-
name,
|
24
|
+
name,
|
25
|
+
value=value,
|
26
|
+
error=error,
|
27
|
+
title=title,
|
28
|
+
token=token,
|
29
|
+
removable=removable,
|
24
30
|
)
|
25
31
|
self.item_type = item_type
|
26
32
|
self.hint = hint
|
@@ -11,11 +11,12 @@ class TextWidget(Widget[str]):
|
|
11
11
|
title: Optional[str],
|
12
12
|
aria_label: Optional[str] = None,
|
13
13
|
placeholder: Optional[str] = None,
|
14
|
+
error: str | None = None,
|
14
15
|
removable: bool = False,
|
15
16
|
value: str = "",
|
16
17
|
token: Optional[str] = None,
|
17
18
|
hint: Optional[str] = None,
|
18
|
-
input_type: str = "text"
|
19
|
+
input_type: str = "text",
|
19
20
|
) -> None:
|
20
21
|
super().__init__(
|
21
22
|
name,
|
@@ -23,6 +24,7 @@ class TextWidget(Widget[str]):
|
|
23
24
|
title=title,
|
24
25
|
aria_label=aria_label,
|
25
26
|
token=token,
|
27
|
+
error=error,
|
26
28
|
removable=removable,
|
27
29
|
)
|
28
30
|
self.placeholder = placeholder or ""
|
@@ -15,12 +15,18 @@ class UnionWidget(Widget[Widget[Any]]):
|
|
15
15
|
*,
|
16
16
|
title: Optional[str],
|
17
17
|
value: Optional[Widget[Any]],
|
18
|
+
error: str | None = None,
|
18
19
|
children_types: Sequence[Type[BaseModel]],
|
19
20
|
token: str,
|
20
21
|
removable: bool,
|
21
22
|
):
|
22
23
|
super().__init__(
|
23
|
-
name,
|
24
|
+
name,
|
25
|
+
value=value,
|
26
|
+
error=error,
|
27
|
+
title=title,
|
28
|
+
token=token,
|
29
|
+
removable=removable,
|
24
30
|
)
|
25
31
|
self.children_types = children_types
|
26
32
|
self.parent_name = name
|