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
@@ -1,525 +0,0 @@
1
- """
2
- Transform.
3
- """
4
-
5
- import secrets
6
- from collections.abc import MutableSequence, Sequence
7
- from decimal import Decimal
8
- from enum import Enum
9
- from inspect import isclass
10
- from types import NoneType
11
- from typing import Any, Literal, Mapping, Type, cast, get_origin
12
- from uuid import UUID
13
-
14
- from markupsafe import Markup
15
- from pydantic import BaseModel, EmailStr, SecretStr, ValidationError
16
- from pydantic.fields import FieldInfo
17
-
18
- from fastlife.adapters.jinjax.widgets.base import Widget
19
- from fastlife.adapters.jinjax.widgets.boolean import BooleanWidget
20
- from fastlife.adapters.jinjax.widgets.checklist import Checkable, ChecklistWidget
21
- from fastlife.adapters.jinjax.widgets.dropdown import DropDownWidget
22
- from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
23
- from fastlife.adapters.jinjax.widgets.model import ModelWidget
24
- from fastlife.adapters.jinjax.widgets.sequence import SequenceWidget
25
- from fastlife.adapters.jinjax.widgets.text import TextWidget
26
- from fastlife.adapters.jinjax.widgets.union import UnionWidget
27
- from fastlife.request.form import FormModel
28
- from fastlife.services.templates import AbstractTemplateRenderer
29
- from fastlife.shared_utils.infer import is_complex_type, is_union
30
-
31
-
32
- class WidgetFactory:
33
- """
34
- Form builder for pydantic model.
35
-
36
- :param renderer: template engine to render widget.
37
- :param token: reuse a token.
38
- """
39
-
40
- def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
41
- self.renderer = renderer
42
- self.token = token or secrets.token_urlsafe(4).replace("_", "-")
43
-
44
- def get_markup(
45
- self,
46
- model: FormModel[Any],
47
- *,
48
- removable: bool = False,
49
- field: FieldInfo | None = None,
50
- ) -> Markup:
51
- return self.get_widget(
52
- model.model.__class__,
53
- model.form_data,
54
- model.errors,
55
- prefix=model.prefix,
56
- removable=removable,
57
- field=field,
58
- ).to_html(self.renderer)
59
-
60
- def get_widget(
61
- self,
62
- base: Type[Any],
63
- form_data: Mapping[str, Any],
64
- form_errors: Mapping[str, Any],
65
- *,
66
- prefix: str,
67
- removable: bool,
68
- field: FieldInfo | None = None,
69
- ) -> Widget[Any]:
70
- return self.build(
71
- base,
72
- value=form_data.get(prefix, {}),
73
- form_errors=form_errors,
74
- name=prefix,
75
- removable=removable,
76
- field=field,
77
- )
78
-
79
- def build(
80
- self,
81
- typ: Type[Any],
82
- *,
83
- name: str = "",
84
- value: Any,
85
- removable: bool,
86
- form_errors: Mapping[str, Any],
87
- field: FieldInfo | None = None,
88
- ) -> Widget[Any]:
89
- if field and field.metadata:
90
- for widget in field.metadata:
91
- if isclass(widget) and issubclass(widget, Widget):
92
- return cast(
93
- Widget[Any],
94
- widget(
95
- name,
96
- value=value,
97
- removable=removable,
98
- title=field.title if field else "",
99
- hint=field.description if field else None,
100
- aria_label=(
101
- field.json_schema_extra.get("aria_label") # type:ignore
102
- if field and field.json_schema_extra
103
- else None
104
- ),
105
- token=self.token,
106
- error=form_errors.get(name),
107
- ),
108
- )
109
-
110
- type_origin = get_origin(typ)
111
- if type_origin:
112
- if is_union(typ):
113
- return self.build_union(name, typ, field, value, form_errors, removable)
114
-
115
- if (
116
- type_origin is Sequence
117
- or type_origin is MutableSequence
118
- or type_origin is list
119
- ):
120
- return self.build_sequence(
121
- name, typ, field, value, form_errors, removable
122
- )
123
-
124
- if type_origin is Literal:
125
- return self.build_literal(
126
- name, typ, field, value, form_errors, removable
127
- )
128
-
129
- if type_origin is set:
130
- return self.build_set(name, typ, field, value, form_errors, removable)
131
-
132
- if issubclass(typ, Enum): # if it raises here, the type_origin is unknown
133
- return self.build_enum(name, typ, field, value, form_errors, removable)
134
-
135
- if issubclass(typ, BaseModel): # if it raises here, the type_origin is unknown
136
- return self.build_model(
137
- name, typ, field, value or {}, form_errors, removable
138
- )
139
-
140
- if issubclass(typ, bool):
141
- return self.build_boolean(
142
- name, typ, field, value or False, form_errors, removable
143
- )
144
-
145
- if issubclass(typ, EmailStr): # type: ignore
146
- return self.build_emailtype(
147
- name, typ, field, value or "", form_errors, removable
148
- )
149
-
150
- if issubclass(typ, SecretStr):
151
- return self.build_secretstr(
152
- name, typ, field, value or "", form_errors, removable
153
- )
154
-
155
- if issubclass(typ, (int, str, float, Decimal, UUID)):
156
- return self.build_simpletype(
157
- name, typ, field, value or "", form_errors, removable
158
- )
159
-
160
- raise NotImplementedError(f"{typ} not implemented") # coverage: ignore
161
-
162
- def build_model(
163
- self,
164
- field_name: str,
165
- typ: Type[BaseModel],
166
- field: FieldInfo | None,
167
- value: Mapping[str, Any],
168
- form_errors: Mapping[str, Any],
169
- removable: bool,
170
- ) -> Widget[Any]:
171
- ret: dict[str, Any] = {}
172
- for key, child_field in typ.model_fields.items():
173
- child_key = f"{field_name}.{key}" if field_name else key
174
- if child_field.exclude:
175
- continue
176
- if child_field.annotation is None:
177
- raise ValueError( # coverage: ignore
178
- f"Missing annotation for {child_field} in {child_key}"
179
- )
180
- ret[key] = self.build(
181
- child_field.annotation,
182
- name=child_key,
183
- field=child_field,
184
- value=value.get(key),
185
- form_errors=form_errors,
186
- removable=False,
187
- )
188
- return ModelWidget(
189
- field_name,
190
- value=list(ret.values()),
191
- removable=removable,
192
- title=field.title if field and field.title else "",
193
- hint=field.description if field else None,
194
- aria_label=(
195
- field.json_schema_extra.get("aria_label") # type:ignore
196
- if field and field.json_schema_extra
197
- else None
198
- ),
199
- token=self.token,
200
- error=form_errors.get(field_name),
201
- nested=field is not None,
202
- )
203
-
204
- def build_union(
205
- self,
206
- field_name: str,
207
- field_type: Type[Any],
208
- field: FieldInfo | None,
209
- value: Any,
210
- form_errors: Mapping[str, Any],
211
- removable: bool,
212
- ) -> Widget[Any]:
213
- types: list[Type[Any]] = []
214
- # required = True
215
- for typ in field_type.__args__: # type: ignore
216
- if typ is NoneType:
217
- # required = False
218
- continue
219
- types.append(typ) # type: ignore
220
-
221
- if (
222
- not removable
223
- and len(types) == 1
224
- # if the optional type is a complex type,
225
- and not is_complex_type(types[0])
226
- ):
227
- return self.build(
228
- types[0],
229
- name=field_name,
230
- field=field,
231
- value=value,
232
- form_errors=form_errors,
233
- removable=False,
234
- )
235
- child = None
236
- if value:
237
- for typ in types:
238
- try:
239
- typ(**value)
240
- except ValidationError:
241
- pass
242
- else:
243
- child = self.build(
244
- typ,
245
- name=field_name,
246
- field=field,
247
- value=value,
248
- form_errors=form_errors,
249
- removable=False,
250
- )
251
-
252
- widget = UnionWidget(
253
- field_name,
254
- # we assume those types are BaseModel
255
- value=child,
256
- children_types=types, # type: ignore
257
- title=field.title if field else "",
258
- hint=field.description if field else None,
259
- aria_label=(
260
- field.json_schema_extra.get("aria_label") # type:ignore
261
- if field and field.json_schema_extra
262
- else None
263
- ),
264
- token=self.token,
265
- removable=removable,
266
- error=form_errors.get(field_name),
267
- )
268
-
269
- return widget
270
-
271
- def build_sequence(
272
- self,
273
- field_name: str,
274
- field_type: Type[Any],
275
- field: FieldInfo | None,
276
- value: Sequence[Any] | None,
277
- form_errors: Mapping[str, Any],
278
- removable: bool,
279
- ) -> Widget[Any]:
280
- typ = field_type.__args__[0] # type: ignore
281
- value = value or []
282
- items = [
283
- self.build(
284
- typ, # type: ignore
285
- name=f"{field_name}.{idx}",
286
- value=v,
287
- field=field,
288
- form_errors=form_errors,
289
- removable=True,
290
- )
291
- for idx, v in enumerate(value)
292
- ]
293
- return SequenceWidget(
294
- field_name,
295
- title=field.title if field else "",
296
- hint=field.description if field else None,
297
- aria_label=(
298
- field.json_schema_extra.get("aria_label") # type:ignore
299
- if field and field.json_schema_extra
300
- else None
301
- ),
302
- value=items,
303
- item_type=typ, # type: ignore
304
- token=self.token,
305
- removable=removable,
306
- error=form_errors.get(field_name),
307
- )
308
-
309
- def build_set(
310
- self,
311
- field_name: str,
312
- field_type: Type[Any],
313
- field: FieldInfo | None,
314
- value: Sequence[Any] | None,
315
- form_errors: Mapping[str, Any],
316
- removable: bool,
317
- ) -> Widget[Any]:
318
- choice_wrapper = field_type.__args__[0]
319
- choices = []
320
- choice_wrapper_origin = get_origin(choice_wrapper)
321
- if choice_wrapper_origin:
322
- if choice_wrapper_origin is Literal:
323
- litchoice: list[str] = choice_wrapper.__args__ # type: ignore
324
- choices = [
325
- Checkable(
326
- label=c,
327
- value=c,
328
- checked=c in value if value else False, # type: ignore
329
- name=field_name,
330
- token=self.token,
331
- error=form_errors.get(f"{field_name}-{c}"),
332
- )
333
- for c in litchoice
334
- ]
335
-
336
- else:
337
- raise NotImplementedError
338
- elif issubclass(choice_wrapper, Enum):
339
- choices = [
340
- Checkable(
341
- label=e.value,
342
- value=e.name,
343
- checked=e.name in value if value else False, # type: ignore
344
- name=field_name,
345
- token=self.token,
346
- error=form_errors.get(f"{field_name}-{e.name}"),
347
- )
348
- for e in choice_wrapper
349
- ]
350
- else:
351
- raise NotImplementedError
352
-
353
- return ChecklistWidget(
354
- field_name,
355
- title=field.title if field else "",
356
- hint=field.description if field else None,
357
- aria_label=(
358
- field.json_schema_extra.get("aria_label") # type:ignore
359
- if field and field.json_schema_extra
360
- else None
361
- ),
362
- token=self.token,
363
- value=choices,
364
- removable=removable,
365
- error=form_errors.get(field_name),
366
- )
367
-
368
- def build_boolean(
369
- self,
370
- field_name: str,
371
- field_type: Type[Any],
372
- field: FieldInfo | None,
373
- value: bool,
374
- form_errors: Mapping[str, Any],
375
- removable: bool,
376
- ) -> Widget[Any]:
377
- return BooleanWidget(
378
- field_name,
379
- removable=removable,
380
- title=field.title if field else "",
381
- hint=field.description if field else None,
382
- aria_label=(
383
- field.json_schema_extra.get("aria_label") # type:ignore
384
- if field and field.json_schema_extra
385
- else None
386
- ),
387
- token=self.token,
388
- value=value,
389
- error=form_errors.get(field_name),
390
- )
391
-
392
- def build_emailtype(
393
- self,
394
- field_name: str,
395
- field_type: Type[Any],
396
- field: FieldInfo | None,
397
- value: str | int | float,
398
- form_errors: Mapping[str, Any],
399
- removable: bool,
400
- ) -> Widget[Any]:
401
- return TextWidget(
402
- field_name,
403
- input_type="email",
404
- placeholder=str(field.examples[0]) if field and field.examples else None,
405
- removable=removable,
406
- title=field.title if field else "",
407
- hint=field.description if field else None,
408
- aria_label=(
409
- field.json_schema_extra.get("aria_label") # type:ignore
410
- if field and field.json_schema_extra
411
- else None
412
- ),
413
- token=self.token,
414
- value=str(value),
415
- error=form_errors.get(field_name),
416
- )
417
-
418
- def build_secretstr(
419
- self,
420
- field_name: str,
421
- field_type: Type[Any],
422
- field: FieldInfo | None,
423
- value: SecretStr | str,
424
- form_errors: Mapping[str, Any],
425
- removable: bool,
426
- ) -> Widget[Any]:
427
- return TextWidget(
428
- field_name,
429
- input_type="password",
430
- placeholder=str(field.examples[0]) if field and field.examples else None,
431
- removable=removable,
432
- title=field.title if field else "",
433
- hint=field.description if field else None,
434
- aria_label=(
435
- field.json_schema_extra.get("aria_label") # type:ignore
436
- if field and field.json_schema_extra
437
- else None
438
- ),
439
- token=self.token,
440
- value=value.get_secret_value() if isinstance(value, SecretStr) else value,
441
- error=form_errors.get(field_name),
442
- )
443
-
444
- def build_literal(
445
- self,
446
- field_name: str,
447
- field_type: Type[Any], # a literal actually
448
- field: FieldInfo | None,
449
- value: str | int | float,
450
- form_errors: Mapping[str, Any],
451
- removable: bool,
452
- ) -> Widget[Any]:
453
- choices: list[str] = field_type.__args__ # type: ignore
454
- if len(choices) == 1:
455
- return HiddenWidget(
456
- field_name,
457
- value=choices[0],
458
- token=self.token,
459
- )
460
- return DropDownWidget(
461
- field_name,
462
- options=choices,
463
- removable=removable,
464
- title=field.title if field else "",
465
- hint=field.description if field else None,
466
- aria_label=(
467
- field.json_schema_extra.get("aria_label") # type:ignore
468
- if field and field.json_schema_extra
469
- else None
470
- ),
471
- token=self.token,
472
- value=str(value),
473
- error=form_errors.get(field_name),
474
- )
475
-
476
- def build_enum(
477
- self,
478
- field_name: str,
479
- field_type: Type[Any], # an enum subclass
480
- field: FieldInfo | None,
481
- value: str | int | float,
482
- form_errors: Mapping[str, Any],
483
- removable: bool,
484
- ) -> Widget[Any]:
485
- options = [(item.name, item.value) for item in field_type] # type: ignore
486
- return DropDownWidget(
487
- field_name,
488
- options=options, # type: ignore
489
- removable=removable,
490
- title=field.title if field else "",
491
- hint=field.description if field else None,
492
- aria_label=(
493
- field.json_schema_extra.get("aria_label") # type:ignore
494
- if field and field.json_schema_extra
495
- else None
496
- ),
497
- token=self.token,
498
- value=str(value),
499
- error=form_errors.get(field_name),
500
- )
501
-
502
- def build_simpletype(
503
- self,
504
- field_name: str,
505
- field_type: Type[Any],
506
- field: FieldInfo | None,
507
- value: str | int | float,
508
- form_errors: Mapping[str, Any],
509
- removable: bool,
510
- ) -> Widget[Any]:
511
- return TextWidget(
512
- field_name,
513
- placeholder=str(field.examples[0]) if field and field.examples else None,
514
- title=field.title if field else "",
515
- hint=field.description if field else None,
516
- aria_label=(
517
- field.json_schema_extra.get("aria_label") # type:ignore
518
- if field and field.json_schema_extra
519
- else None
520
- ),
521
- removable=removable,
522
- token=self.token,
523
- value=str(value),
524
- error=form_errors.get(field_name),
525
- )