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.
- fastlife/adapters/jinjax/renderer.py +44 -15
- 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/hidden.py +2 -0
- fastlife/adapters/jinjax/widgets/model.py +2 -0
- fastlife/components/Form.jinja +12 -0
- fastlife/config/configurator.py +15 -15
- fastlife/config/exceptions.py +2 -0
- fastlife/config/resources.py +2 -2
- fastlife/config/settings.py +2 -0
- fastlife/middlewares/reverse_proxy/x_forwarded.py +7 -8
- fastlife/services/policy.py +1 -1
- fastlife/services/translations.py +12 -6
- fastlife/shared_utils/resolver.py +58 -1
- fastlife/testing/dom.py +140 -0
- fastlife/testing/form.py +204 -0
- fastlife/testing/session.py +67 -0
- fastlife/testing/testclient.py +4 -387
- {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
- {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +33 -18
- fastlife/adapters/jinjax/widgets/factory.py +0 -525
- {fastlifeweb-0.16.4.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
- {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.
|
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
|
[](https://mardiros.github.io/fastlife/)
|
38
38
|
[](https://github.com/mardiros/fastlife/actions/workflows/main.yml)
|
39
39
|
[](https://codecov.io/gh/mardiros/fastlife)
|
40
|
-
|
40
|
+
[](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
|
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
|
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=
|
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/
|
11
|
-
fastlife/adapters/jinjax/widgets/
|
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=
|
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=
|
1670
|
-
fastlife/config/exceptions.py,sha256=
|
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=
|
1674
|
-
fastlife/config/settings.py,sha256=
|
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=
|
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=
|
1709
|
+
fastlife/services/policy.py,sha256=RfYGPjfEAAoHECUnZVLPZgN0iRanu8UKQSky6oAz81o,1687
|
1698
1710
|
fastlife/services/templates.py,sha256=-dIt8zrgiRsjMblS174Rx_2xRZkQQRIATYhaA2vbIAk,3867
|
1699
|
-
fastlife/services/translations.py,sha256=
|
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=
|
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/
|
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.
|
1711
|
-
fastlifeweb-0.
|
1712
|
-
fastlifeweb-0.
|
1713
|
-
fastlifeweb-0.
|
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
|
-
)
|
File without changes
|
File without changes
|