fastlifeweb 0.16.4__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 (34) hide show
  1. fastlife/adapters/jinjax/renderer.py +44 -15
  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/hidden.py +2 -0
  16. fastlife/adapters/jinjax/widgets/model.py +2 -0
  17. fastlife/components/Form.jinja +12 -0
  18. fastlife/config/configurator.py +15 -15
  19. fastlife/config/exceptions.py +2 -0
  20. fastlife/config/resources.py +2 -2
  21. fastlife/config/settings.py +2 -0
  22. fastlife/middlewares/reverse_proxy/x_forwarded.py +7 -8
  23. fastlife/services/policy.py +1 -1
  24. fastlife/services/translations.py +12 -6
  25. fastlife/shared_utils/resolver.py +58 -1
  26. fastlife/testing/dom.py +140 -0
  27. fastlife/testing/form.py +204 -0
  28. fastlife/testing/session.py +67 -0
  29. fastlife/testing/testclient.py +4 -387
  30. {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
  31. {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +33 -18
  32. fastlife/adapters/jinjax/widgets/factory.py +0 -525
  33. {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
  34. {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.16.4
3
+ Version: 0.17.0
4
4
  Summary: High-level web framework
5
5
  Home-page: https://github.com/mardiros/fastlife
6
6
  License: BSD-derived
@@ -37,24 +37,24 @@ Description-Content-Type: text/markdown
37
37
  [![Documentation](https://github.com/mardiros/fastlife/actions/workflows/gh-pages.yml/badge.svg)](https://mardiros.github.io/fastlife/)
38
38
  [![Continuous Integration](https://github.com/mardiros/fastlife/actions/workflows/main.yml/badge.svg)](https://github.com/mardiros/fastlife/actions/workflows/main.yml)
39
39
  [![Coverage Report](https://codecov.io/gh/mardiros/fastlife/graph/badge.svg?token=DTpi73d7mf)](https://codecov.io/gh/mardiros/fastlife)
40
-
40
+ [![Maintainability](https://api.codeclimate.com/v1/badges/94d107797b15b5e8843e/maintainability)](https://codeclimate.com/github/mardiros/fastlife/maintainability)
41
41
 
42
42
  > ⚠️ **Under Heavy Development**
43
43
  > Please note that this project is still in active development. Features and APIs may change frequently.
44
44
  > Even the name is not definitive.
45
45
 
46
- An opinionated Python web framework (based on FastAPI).
46
+ An opinionated Python web framework (based on {term}`FastAPI`).
47
47
 
48
48
  ## Purpose
49
49
 
50
50
  Fastlife helps at building Web Application with session, security, html test client,
51
51
  and html form generated from pydantic schema using customizable widget.
52
52
 
53
- Templates are made using [JinjaX](https://jinjax.scaletti.dev/) and an extensible [set of
53
+ Templates are made using {term}`JinjaX` and an extensible [set of
54
54
  component](https://mardiros.github.io/fastlife/components/index.html) is available
55
55
  in order to build pages.
56
56
 
57
- Those components are currently stylized by [tailwindcss](https://tailwindcss.com/),
57
+ Those components are currently stylized by {term}`Tailwind CSS`,
58
58
  using [pytailwindcss](https://github.com/timonweb/pytailwindcss).
59
59
 
60
60
  Moreover, you can also write API, in an opinionated way to enforce documentation
@@ -63,7 +63,7 @@ consistency.
63
63
 
64
64
  ## First class configuration.
65
65
 
66
- Fastlife is adding a "Configurator", like Pyramid to get a better scallable codebase.
66
+ Fastlife is adding a "Configurator", like {term}`Pyramid` to get a better scallable codebase.
67
67
 
68
68
  The configurator in fastlife organizes configuration settings hierarchically,
69
69
  enabling easy management and overriding at different levels.
@@ -1,15 +1,27 @@
1
1
  fastlife/__init__.py,sha256=fokakuhI0fdAjHP5w6GWi-YfCx7iTnrVzjSyZ11Cdgg,676
2
2
  fastlife/adapters/__init__.py,sha256=WYjEN8gp4r7LCHqmIO5VzzvsT8QGRE3w4G47UwYDtAo,94
3
3
  fastlife/adapters/jinjax/__init__.py,sha256=jy88zyqk7nFlaY-0lmgAoe0HyO5r_NKckQb3faQiUv4,137
4
- fastlife/adapters/jinjax/renderer.py,sha256=_8k4a4R4ExjJrKiG5m5bFOxYqOFa3_TtmZ7iU1rd-p8,13041
4
+ fastlife/adapters/jinjax/renderer.py,sha256=FOalTMnJ9kq_lqiiOnWq5N0x7RIYSlRZOWhux5F3RnU,14164
5
+ fastlife/adapters/jinjax/widget_factory/__init__.py,sha256=Dy_2xr_YDAyEF9WtNpjV-aYaehRO1iKEIHVFdfFeszw,59
6
+ fastlife/adapters/jinjax/widget_factory/base.py,sha256=cRVk2VqpQ7ZfrOslcJQD3eju3gGl2fACMWfcFyBPahs,1009
7
+ fastlife/adapters/jinjax/widget_factory/bool_builder.py,sha256=2-Hv5w4hfBfGWGetb00I8Lm1FDAputH2MNt3tCx-RbA,1280
8
+ fastlife/adapters/jinjax/widget_factory/emailstr_builder.py,sha256=GjRCT_kq9D6ZSu_Qs7ef2nj8gfaZMT0J-SpEj6NZWOg,1472
9
+ fastlife/adapters/jinjax/widget_factory/enum_builder.py,sha256=xMZpxik9zmpZbMTcldOHQRXYscNO-9YlcduY3GpFEQI,1516
10
+ fastlife/adapters/jinjax/widget_factory/factory.py,sha256=tD4RrOgWLqD1_R2ZVHiKDOdPCV5JDN3e6SgpklehBhQ,5649
11
+ fastlife/adapters/jinjax/widget_factory/literal_builder.py,sha256=mk8cmXDah_WRpy6wTRA6_du7UV6vxHoDb9ujgAAxH44,1677
12
+ fastlife/adapters/jinjax/widget_factory/model_builder.py,sha256=cqrb7zkJHy0r4angYRYnz5hyHx99EL4MbNl-sS6qq8I,2220
13
+ fastlife/adapters/jinjax/widget_factory/secretstr_builder.py,sha256=DrFXJeoajai7r1qfq8kBavdoo33-9DImmM4u8l_MKfQ,1562
14
+ fastlife/adapters/jinjax/widget_factory/sequence_builder.py,sha256=97aJ4K_pm1zDr_xNYUoO9UeziRT4VeFKIdkZ1gAcjdM,1928
15
+ fastlife/adapters/jinjax/widget_factory/set_builder.py,sha256=Qulao7i7pJNF1ZRzFdpJ-onQ2faW8IjACOi2sZyoYzA,2731
16
+ fastlife/adapters/jinjax/widget_factory/simpletype_builder.py,sha256=OFkbF_5_9DP56VQLXRXGi6_cm_6JmFgCzdCblnBb1aE,1670
17
+ fastlife/adapters/jinjax/widget_factory/union_builder.py,sha256=-FlqGejzfEiyKb8vgSaMbECr8libC4BprK6F2OA_12M,2825
5
18
  fastlife/adapters/jinjax/widgets/__init__.py,sha256=HERnX9xiXUbTDz3XtlnHWABTBjhIq_kkBgWs5E6ZIMY,42
6
19
  fastlife/adapters/jinjax/widgets/base.py,sha256=3bBThRMnsdCi6Q_Dm73ep5pNOqgpSXsvAIBbHshfY7I,4037
7
20
  fastlife/adapters/jinjax/widgets/boolean.py,sha256=w4hZMo_8xDoThStlIUR4eVfLm8JwUp0-TaGCjGSyCbA,1145
8
21
  fastlife/adapters/jinjax/widgets/checklist.py,sha256=8fgOrdxy1xpyQ6p3_mbRMd2vx6EU2WT5jI7QF27Y5EQ,1664
9
22
  fastlife/adapters/jinjax/widgets/dropdown.py,sha256=3Kc7i0z-7d6HrQchSHFCO5-xOh3bSEePo_pjXrIkvSE,1599
10
- fastlife/adapters/jinjax/widgets/factory.py,sha256=42cadaEzQb5vrwdOJRpv8JATz97EHa6YZo2EIbDx36o,17554
11
- fastlife/adapters/jinjax/widgets/hidden.py,sha256=pkMKxKhBKSGNf1Su81Jr-n8BJ45X5Qjsd1xXnJ7prPI,699
12
- fastlife/adapters/jinjax/widgets/model.py,sha256=xdgY--K4GNo5IIWTLjSAnNRDHq2bt81mh9O5J23y0gg,1299
23
+ fastlife/adapters/jinjax/widgets/hidden.py,sha256=ZOJoUwMMgyabTFII38lnr8QRgVo370Go0VZ4qhEW1zc,720
24
+ fastlife/adapters/jinjax/widgets/model.py,sha256=t9A3C8wcptxvf7Mlrx9mUraxnG2p_39CrGnRq71t-A0,1322
13
25
  fastlife/adapters/jinjax/widgets/sequence.py,sha256=60rgz4LgE_TQQwajiZhn6EhY-s-HXOiIdQiQoKlUCvQ,1533
14
26
  fastlife/adapters/jinjax/widgets/text.py,sha256=KtUieF-q_BigG5AcL-4Sdr6LrIOQdWPwlaVW-2p-KPQ,3205
15
27
  fastlife/adapters/jinjax/widgets/union.py,sha256=CO6Q4_U8DieVsS5NzMp6TAbVXrBljfcjSARycEKYPDY,2540
@@ -18,7 +30,7 @@ fastlife/components/Button.jinja,sha256=COtCjDpzGLNqtBUYsHw7gdUay4kff3KdLJFAzrEn
18
30
  fastlife/components/Checkbox.jinja,sha256=47_E9uPdr3QKUvRVhNQA7VE0uh5FVslQM26cdF0WCtY,753
19
31
  fastlife/components/CsrfToken.jinja,sha256=ftqhcMibf1G8pbGCytlUcj5LGEmD8QJKwVKTro5w-ns,199
20
32
  fastlife/components/Details.jinja,sha256=BKyhSU7bZdbd_deTjmAGcMbgUoQW3h8JSR3thH-2oJA,741
21
- fastlife/components/Form.jinja,sha256=B-l1c-Phe86Y0HfzJHvn-QFljHpsF0gEJcJ6PowuFEI,1520
33
+ fastlife/components/Form.jinja,sha256=Wb0nK5xuhqhkuQll9j76i3nBJcYCIjXG9RE9nWeoPZc,2095
22
34
  fastlife/components/H1.jinja,sha256=ODwQMgwtuy2E2ShgamjFDlnCwOQQuuLIhvEzUF66nYM,375
23
35
  fastlife/components/H2.jinja,sha256=LcBE2R_N50gio01nxH9qhp8_G1HxOT91xG_u8J8ae_Q,375
24
36
  fastlife/components/H3.jinja,sha256=3PzxfQh07A35Og6dE5ow9BZdNp2Qnu7OuVeWREvD7Uo,375
@@ -1666,17 +1678,17 @@ fastlife/components/pydantic_form/Textarea.jinja,sha256=NzfCi5agRUSVcb5RXw0QamM8
1666
1678
  fastlife/components/pydantic_form/Union.jinja,sha256=czTska54z9KCZKu-FaycLmOvtH6y6CGUFQ8DHnkjrJk,1461
1667
1679
  fastlife/components/pydantic_form/Widget.jinja,sha256=EXskDqt22D5grpGVwlZA3ndve2Wr_6yQH4qVE9c31Og,397
1668
1680
  fastlife/config/__init__.py,sha256=ThosRIPZ_fpD0exZu-kUC_f8ZNa5KyDlleWMmEHkjEo,448
1669
- fastlife/config/configurator.py,sha256=keVPDTiCVko8Ncn1yYAnhG0x9Zp5J9yxMKEeyXZYP1E,22351
1670
- fastlife/config/exceptions.py,sha256=M45w7ZNAj4KPPHNYiCtRHulRRb08rkOGtJ2I8WoXNHI,1207
1681
+ fastlife/config/configurator.py,sha256=ooV2NJJB830GWRMMqkBIGMmqcGg0cEId2_HAqvqCLxg,22384
1682
+ fastlife/config/exceptions.py,sha256=kH2-akbzGeODlY_1bUhbzDKqBFrpOoqnVom0WPm0IGg,1237
1671
1683
  fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
1672
1684
  fastlife/config/registry.py,sha256=dGcNm7E6WY0x5HZNzo1gBFvGFCWeJj6JFXsJtLax5NU,1347
1673
- fastlife/config/resources.py,sha256=XJIJxNtB5DAxXvDOU3xbJFa7_n-rAy9MXXjwekvj6p8,8583
1674
- fastlife/config/settings.py,sha256=7oggPOucyJwQYI97q8vs3kPXjFIVpQu1q6BK25h-uFs,3789
1685
+ fastlife/config/resources.py,sha256=Wu3vVr7XD18Gf4-MYYCxAAnuRmsAJmpllonts_BVGdQ,8593
1686
+ fastlife/config/settings.py,sha256=ecVczScdSJKOoXxE3ToQCcrK2AbHIXFVKKvf4jHd7TM,3902
1675
1687
  fastlife/config/views.py,sha256=V-P53GSnvqEPzkvEWNuI4ofcdbFur2Dl-s6BeKXObwI,2086
1676
1688
  fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
1677
1689
  fastlife/middlewares/base.py,sha256=9OYqByRuVoIrLt353NOedPQTLdr7LSmxhb2BZcp20qk,638
1678
1690
  fastlife/middlewares/reverse_proxy/__init__.py,sha256=g1SoVDmenKzpAAPYHTEsWgdBByOxtLg9fGx6RV3i0ok,846
1679
- fastlife/middlewares/reverse_proxy/x_forwarded.py,sha256=sGX7b4hOAgsG2KmtL3xlrZt61_IpF1exf6x_33F_QXE,1733
1691
+ fastlife/middlewares/reverse_proxy/x_forwarded.py,sha256=0O9tziA63gQBmKATQz3B8H8G9CjZjnfM9NaisrvJHRY,1714
1680
1692
  fastlife/middlewares/session/__init__.py,sha256=3XgXcIO6yQls5G7x8K2T8b7a_enA_7rQptWZcp3j2Ak,1400
1681
1693
  fastlife/middlewares/session/middleware.py,sha256=R48x3MJ-tu8siy8G12hDHa83sMcZz6E1eEb0xwk77E4,3166
1682
1694
  fastlife/middlewares/session/serializer.py,sha256=wpaktDP5v1spmbD-D3Q68EK9A0KInE4DT8mkogBJ3Fc,2157
@@ -1694,20 +1706,23 @@ fastlife/security/csrf.py,sha256=PIKG83LPqKz4kDALnZxIyPdYVwbNqsIryi7JPqRPQag,216
1694
1706
  fastlife/security/policy.py,sha256=ECNEyZXjizK2kz61v5eU7xFNd_M6tIlr9JEwcdyjuj8,5142
1695
1707
  fastlife/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1696
1708
  fastlife/services/locale_negociator.py,sha256=Np2O8s7xnYTpf5eCG7LvcfFJ2LV7p_k86NNrU9Lju88,846
1697
- fastlife/services/policy.py,sha256=bHe49EiD2ExQpFJxOsno6WyjxNrn8pf0wWDixkToHn0,1680
1709
+ fastlife/services/policy.py,sha256=RfYGPjfEAAoHECUnZVLPZgN0iRanu8UKQSky6oAz81o,1687
1698
1710
  fastlife/services/templates.py,sha256=-dIt8zrgiRsjMblS174Rx_2xRZkQQRIATYhaA2vbIAk,3867
1699
- fastlife/services/translations.py,sha256=0MD6R4xCJM40JxvLyCg5laXEPJlyKvzjhVRkjE5l010,4383
1711
+ fastlife/services/translations.py,sha256=Fu93zSc3ajNVFfAqw_G0nBV9bitss9Xy-he9lSHx0V8,4387
1700
1712
  fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
1701
1713
  fastlife/shared_utils/infer.py,sha256=3G_u6q2aWzeiVlAyGaWIlnAcz90m4bFNwpPYd5JIqfE,723
1702
- fastlife/shared_utils/resolver.py,sha256=Nnva8D_BM_REFxH1sXZYKUZ5Ryx6o6vjqBSgvA7qWLY,1778
1714
+ fastlife/shared_utils/resolver.py,sha256=Wb9cO2MWavpti63hju15xmwFMgaD5DsQaxikRpB39E8,3713
1703
1715
  fastlife/templates/__init__.py,sha256=QrP_5UAOgxqC-jOu5tcjd-l6GOYrS4dka6vmWMxWqfo,184
1704
1716
  fastlife/templates/binding.py,sha256=0pE2btOwLf4xOEgBXVOyz_dIX9tBCYCaJ7RhZI3knbs,1464
1705
1717
  fastlife/templates/constants.py,sha256=MGdUjkF9hsPMN8rOS49eWbAApcb8FL-FAeFvJU8k90M,8387
1706
1718
  fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,99
1707
- fastlife/testing/testclient.py,sha256=3s0CguxV3AePBRQ5XccutS0sMZtUkxvkt3C7z6IJbz0,20600
1719
+ fastlife/testing/dom.py,sha256=dVzDoZokn-ii681UaEwAr-khM5KE-CHgXSSLSo24oH0,4489
1720
+ fastlife/testing/form.py,sha256=ST0xNCoUqz_oD92cWHzQ6CbJ5hFopvu_NNKpOfiuYWY,7874
1721
+ fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
1722
+ fastlife/testing/testclient.py,sha256=WmUnGkDPuSd4dKzTiXWyHWlJ31zBbySvMH9m8p0acg8,6741
1708
1723
  fastlife/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1709
1724
  fastlife/views/pydantic_form.py,sha256=4dv37JORLpvkgCgMGZfUN_qy7wme040GLZAzOTFqdnU,1367
1710
- fastlifeweb-0.16.4.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
1711
- fastlifeweb-0.16.4.dist-info/METADATA,sha256=pFRnHJnvWXf7-ZucfCFAqkdPQ_VPE9u3MdjeH5RQSJo,3345
1712
- fastlifeweb-0.16.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1713
- fastlifeweb-0.16.4.dist-info/RECORD,,
1725
+ fastlifeweb-0.17.0.dist-info/LICENSE,sha256=F75xSseSKMwqzFj8rswYU6NWS3VoWOc_gY3fJYf9_LI,1504
1726
+ fastlifeweb-0.17.0.dist-info/METADATA,sha256=xw0ntJeslB74GTpWv_9HlWRQgNlQ7HyAulG1FkKEQic,3480
1727
+ fastlifeweb-0.17.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
1728
+ fastlifeweb-0.17.0.dist-info/RECORD,,
@@ -1,525 +0,0 @@
1
- """
2
- Transform.
3
- """
4
-
5
- import secrets
6
- from collections.abc import Mapping, 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, 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
- )