fastlifeweb 0.20.1__py3-none-any.whl → 0.22.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.
- CHANGELOG.md +18 -1
- fastlife/__init__.py +45 -13
- fastlife/adapters/__init__.py +1 -1
- fastlife/adapters/fastapi/__init__.py +9 -0
- fastlife/adapters/fastapi/form.py +26 -0
- fastlife/{request → adapters/fastapi}/form_data.py +1 -1
- fastlife/{request → adapters/fastapi}/localizer.py +4 -2
- fastlife/adapters/fastapi/request.py +33 -0
- fastlife/{routing → adapters/fastapi/routing}/route.py +3 -3
- fastlife/{routing → adapters/fastapi/routing}/router.py +1 -1
- fastlife/adapters/itsdangerous/__init__.py +3 -0
- fastlife/adapters/itsdangerous/session.py +50 -0
- fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py +7 -7
- fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py +1 -1
- fastlife/adapters/jinjax/renderer.py +9 -57
- fastlife/adapters/jinjax/widget_factory/bool_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +5 -4
- fastlife/adapters/jinjax/widget_factory/enum_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/factory.py +32 -23
- fastlife/adapters/jinjax/widget_factory/literal_builder.py +7 -6
- fastlife/adapters/jinjax/widget_factory/model_builder.py +3 -3
- fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/sequence_builder.py +3 -3
- fastlife/adapters/jinjax/widget_factory/set_builder.py +2 -2
- fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +7 -8
- fastlife/adapters/jinjax/widget_factory/union_builder.py +3 -3
- fastlife/adapters/jinjax/widgets/base.py +36 -36
- fastlife/adapters/jinjax/widgets/boolean.py +13 -34
- fastlife/adapters/jinjax/widgets/checklist.py +36 -42
- fastlife/adapters/jinjax/widgets/dropdown.py +32 -38
- fastlife/adapters/jinjax/widgets/hidden.py +7 -15
- fastlife/adapters/jinjax/widgets/model.py +36 -43
- fastlife/adapters/jinjax/widgets/sequence.py +63 -42
- fastlife/adapters/jinjax/widgets/text.py +39 -78
- fastlife/adapters/jinjax/widgets/union.py +51 -58
- fastlife/components/CsrfToken.jinja +1 -1
- fastlife/components/Form.jinja +1 -1
- fastlife/components/pydantic_form/FatalError.jinja +8 -0
- fastlife/components/pydantic_form/Widget.jinja +4 -3
- fastlife/config/__init__.py +3 -6
- fastlife/config/configurator.py +80 -32
- fastlife/config/exceptions.py +0 -2
- fastlife/config/resources.py +1 -2
- fastlife/config/views.py +2 -4
- fastlife/domain/__init__.py +1 -0
- fastlife/domain/model/__init__.py +1 -0
- fastlife/domain/model/asgi.py +3 -0
- fastlife/domain/model/csrf.py +19 -0
- fastlife/{request → domain/model}/form.py +13 -22
- fastlife/{request → domain/model}/request.py +26 -30
- fastlife/domain/model/security_policy.py +105 -0
- fastlife/{templates/inline.py → domain/model/template.py} +8 -0
- fastlife/domain/model/types.py +17 -0
- fastlife/middlewares/base.py +1 -1
- fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -2
- fastlife/middlewares/session/__init__.py +2 -2
- fastlife/middlewares/session/middleware.py +4 -3
- fastlife/middlewares/session/serializer.py +0 -44
- fastlife/{services/policy.py → service/check_permission.py} +1 -1
- fastlife/{security → service}/csrf.py +5 -15
- fastlife/{services → service}/locale_negociator.py +5 -8
- fastlife/{config → service}/registry.py +13 -7
- fastlife/service/security_policy.py +100 -0
- fastlife/{services → service}/templates.py +10 -48
- fastlife/{services → service}/translations.py +15 -0
- fastlife/{config/settings.py → settings.py} +6 -12
- fastlife/shared_utils/infer.py +24 -1
- fastlife/{templates/constants.py → template_globals.py} +2 -2
- fastlife/testing/testclient.py +2 -2
- fastlife/views/__init__.py +1 -0
- fastlife/views/pydantic_form.py +6 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +79 -80
- tailwind.config.js +1 -1
- fastlife/components/pydantic_form/Boolean.jinja +0 -13
- fastlife/components/pydantic_form/Checklist.jinja +0 -21
- fastlife/components/pydantic_form/Dropdown.jinja +0 -18
- fastlife/components/pydantic_form/Hidden.jinja +0 -3
- fastlife/components/pydantic_form/Model.jinja +0 -30
- fastlife/components/pydantic_form/Sequence.jinja +0 -47
- fastlife/components/pydantic_form/Text.jinja +0 -11
- fastlife/components/pydantic_form/Textarea.jinja +0 -38
- fastlife/components/pydantic_form/Union.jinja +0 -34
- fastlife/request/__init__.py +0 -5
- fastlife/security/__init__.py +0 -1
- fastlife/security/policy.py +0 -188
- fastlife/templates/__init__.py +0 -12
- fastlife/templates/binding.py +0 -52
- /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
- /fastlife/{services → service}/__init__.py +0 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.20.1.dist-info → fastlifeweb-0.22.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## 0.22.0 - Released on 2024-11-23
|
2
|
+
* Add a way to add fatal errors on form in order to display an error block.
|
3
|
+
* The localizer can be called gettext in the depency in order to simple translation.
|
4
|
+
* Expose the 99% of the usefull API in the main package.
|
5
|
+
* Refactor all internal class to get a more hexagonal approach in order to reduce
|
6
|
+
circular dependencies.
|
7
|
+
|
8
|
+
## 0.21.0 - Released on 2024-11-15
|
9
|
+
* Make the InlineTemplate the only way to render views template.
|
10
|
+
* Breaking change: template args is not supported in Configutor.add_route.
|
11
|
+
* Breaking change: template args is not supported in @view_config.
|
12
|
+
* Breaking change: template and Template dedendencies have been removed.
|
13
|
+
* Add new method in the configurator to register global vars for template:
|
14
|
+
{meth}`fastlife.config.configurator.GenericConfigurator.add_renderer_global`.
|
15
|
+
* Add npgettext i18n helper method support.
|
16
|
+
* Remove babel from dependency list (only a dev dependency).
|
17
|
+
|
1
18
|
## 0.20.1 - Released on 2024-11-09
|
2
19
|
* Add a new class GenericRegistry in order to properly type custom Configurator / Registry / Settings
|
3
20
|
* Using InlineTemplate, we can pass arbitrary types for pydantic form
|
@@ -41,7 +58,7 @@
|
|
41
58
|
* Hotfix components to create tables
|
42
59
|
|
43
60
|
## 0.15.0 - Released on 2024-09-29
|
44
|
-
* Add an {class}`fastlife.
|
61
|
+
* Add an {class}`fastlife.service.security_policy.AbstractSecurityPolicy` class
|
45
62
|
* New method {meth}`fastlife.config.configurator.GenericConfigurator.set_security_policy`
|
46
63
|
* Breaking change, the check_permission has been removed from the settings.
|
47
64
|
to configure the permission policy, a security policy has to be implemented.
|
fastlife/__init__.py
CHANGED
@@ -3,40 +3,72 @@ from importlib import metadata
|
|
3
3
|
__version__ = metadata.version("fastlifeweb")
|
4
4
|
|
5
5
|
from fastapi import Response
|
6
|
+
from fastapi.responses import RedirectResponse
|
6
7
|
|
8
|
+
from .adapters.fastapi.form import form_model
|
9
|
+
from .adapters.fastapi.localizer import Localizer
|
10
|
+
from .adapters.fastapi.request import AnyRequest, Registry, Request, get_request
|
7
11
|
from .config import (
|
8
12
|
Configurator,
|
9
|
-
DefaultRegistry,
|
10
13
|
GenericConfigurator,
|
11
|
-
GenericRegistry,
|
12
|
-
Settings,
|
13
14
|
configure,
|
15
|
+
exception_handler,
|
14
16
|
resource,
|
15
17
|
resource_view,
|
16
18
|
view_config,
|
17
19
|
)
|
18
|
-
from .
|
20
|
+
from .domain.model.form import FormModel
|
21
|
+
from .domain.model.request import GenericRequest
|
22
|
+
from .domain.model.security_policy import (
|
23
|
+
Allowed,
|
24
|
+
Denied,
|
25
|
+
Forbidden,
|
26
|
+
HasPermission,
|
27
|
+
Unauthenticated,
|
28
|
+
Unauthorized,
|
29
|
+
)
|
30
|
+
from .domain.model.template import JinjaXTemplate
|
19
31
|
|
20
32
|
# from .request.form_data import model
|
21
|
-
from .
|
33
|
+
from .service.registry import DefaultRegistry, GenericRegistry
|
34
|
+
from .service.security_policy import AbstractSecurityPolicy, InsecurePolicy
|
35
|
+
from .settings import Settings
|
22
36
|
|
23
37
|
__all__ = [
|
24
38
|
# Config
|
25
|
-
"configure",
|
26
39
|
"GenericConfigurator",
|
27
|
-
"Configurator",
|
28
|
-
"DefaultRegistry",
|
29
40
|
"GenericRegistry",
|
30
|
-
"
|
41
|
+
"Registry",
|
31
42
|
"Settings",
|
43
|
+
"configure",
|
32
44
|
"view_config",
|
45
|
+
"exception_handler",
|
33
46
|
"resource",
|
34
47
|
"resource_view",
|
35
|
-
|
36
|
-
|
37
|
-
|
48
|
+
"Configurator",
|
49
|
+
"DefaultRegistry",
|
50
|
+
# Form
|
51
|
+
"FormModel",
|
52
|
+
"form_model",
|
53
|
+
# Request
|
38
54
|
"GenericRequest",
|
55
|
+
"AnyRequest",
|
56
|
+
"Request",
|
39
57
|
"get_request",
|
40
|
-
|
58
|
+
# Response
|
41
59
|
"Response",
|
60
|
+
"RedirectResponse",
|
61
|
+
# Security
|
62
|
+
"AbstractSecurityPolicy",
|
63
|
+
"HasPermission",
|
64
|
+
"Unauthenticated",
|
65
|
+
"Allowed",
|
66
|
+
"Denied",
|
67
|
+
"Unauthorized",
|
68
|
+
"Forbidden",
|
69
|
+
"InsecurePolicy",
|
70
|
+
# Template
|
71
|
+
"JinjaXTemplate",
|
72
|
+
# i18n
|
73
|
+
"Localizer",
|
42
74
|
]
|
fastlife/adapters/__init__.py
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
"""HTTP Form serialization."""
|
2
|
+
|
3
|
+
from collections.abc import Callable, Mapping
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from fastapi import Depends
|
7
|
+
|
8
|
+
from fastlife.adapters.fastapi.form_data import MappingFormData
|
9
|
+
from fastlife.adapters.fastapi.request import Registry
|
10
|
+
from fastlife.domain.model.form import FormModel, T
|
11
|
+
|
12
|
+
|
13
|
+
def form_model(
|
14
|
+
cls: type[T], name: str | None = None
|
15
|
+
) -> Callable[[Mapping[str, Any]], FormModel[T]]:
|
16
|
+
"""
|
17
|
+
Build a model, a class of type T based on Pydandic Base Model from a form payload.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def to_model(data: MappingFormData, registry: Registry) -> FormModel[T]:
|
21
|
+
prefix = name or registry.settings.form_data_model_prefix
|
22
|
+
if not data:
|
23
|
+
return FormModel[T].default(prefix, cls)
|
24
|
+
return FormModel[T].from_payload(prefix, cls, data)
|
25
|
+
|
26
|
+
return Depends(to_model)
|
@@ -2,12 +2,14 @@ from typing import Annotated
|
|
2
2
|
|
3
3
|
from fastapi import Depends
|
4
4
|
|
5
|
-
from fastlife.
|
6
|
-
from fastlife.
|
5
|
+
from fastlife.adapters.fastapi.request import Request
|
6
|
+
from fastlife.service.translations import Localizer as RequestLocalizer
|
7
7
|
|
8
8
|
|
9
9
|
def get_localizer(request: Request) -> RequestLocalizer:
|
10
|
+
"""Return the localizer for the given request."""
|
10
11
|
return request.registry.localizer(request)
|
11
12
|
|
12
13
|
|
13
14
|
Localizer = Annotated[RequestLocalizer, Depends(get_localizer)]
|
15
|
+
"""Define a localizer"""
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""HTTP Request representation in a python object."""
|
2
|
+
|
3
|
+
from typing import Annotated, Any
|
4
|
+
|
5
|
+
from fastapi import Request as FastAPIRequest
|
6
|
+
from fastapi.params import Depends
|
7
|
+
|
8
|
+
from fastlife.domain.model.request import GenericRequest
|
9
|
+
from fastlife.service.registry import DefaultRegistry
|
10
|
+
|
11
|
+
|
12
|
+
def get_request(request: FastAPIRequest) -> GenericRequest[Any]:
|
13
|
+
"""Return the Fastlife Request object."""
|
14
|
+
return request # type: ignore
|
15
|
+
|
16
|
+
|
17
|
+
Request = Annotated[GenericRequest[DefaultRegistry], Depends(get_request)]
|
18
|
+
"""A request that is associated to the default registry."""
|
19
|
+
# FastAPI handle its Request objects using a lenient_issubclass,
|
20
|
+
# basically a issubclass(Request), doe to the Generic[T], it does not work.
|
21
|
+
|
22
|
+
|
23
|
+
AnyRequest = Annotated[GenericRequest[Any], Depends(get_request)]
|
24
|
+
"""A request version that is associated to the any registry."""
|
25
|
+
|
26
|
+
|
27
|
+
def get_registry(request: Request) -> DefaultRegistry:
|
28
|
+
"""Return the Fastlife Registry object."""
|
29
|
+
return request.registry
|
30
|
+
|
31
|
+
|
32
|
+
Registry = Annotated[DefaultRegistry, Depends(get_registry)]
|
33
|
+
"""FastAPI dependency to access to the global registry."""
|
@@ -7,10 +7,10 @@ from fastapi.routing import APIRoute
|
|
7
7
|
from starlette.requests import Request as StarletteRequest
|
8
8
|
from starlette.responses import Response
|
9
9
|
|
10
|
-
from fastlife.
|
10
|
+
from fastlife.domain.model.request import GenericRequest
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
|
-
from fastlife.
|
13
|
+
from fastlife.service.registry import DefaultRegistry # coverage: ignore
|
14
14
|
|
15
15
|
|
16
16
|
class Route(APIRoute):
|
@@ -41,7 +41,7 @@ class Route(APIRoute):
|
|
41
41
|
orig_route_handler = super().get_route_handler()
|
42
42
|
|
43
43
|
async def route_handler(request: StarletteRequest) -> Response:
|
44
|
-
req =
|
44
|
+
req = GenericRequest(self._registry, request)
|
45
45
|
return await orig_route_handler(req)
|
46
46
|
|
47
47
|
return route_handler
|
@@ -0,0 +1,50 @@
|
|
1
|
+
"""Serialize session."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from base64 import b64decode, b64encode
|
5
|
+
from collections.abc import Mapping
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
import itsdangerous
|
9
|
+
|
10
|
+
from fastlife.middlewares.session.serializer import AbsractSessionSerializer
|
11
|
+
|
12
|
+
|
13
|
+
class SignedSessionSerializer(AbsractSessionSerializer):
|
14
|
+
"""
|
15
|
+
The default fastlife session serializer.
|
16
|
+
|
17
|
+
It's based on the itsdangerous package to sign the session with a secret key.
|
18
|
+
|
19
|
+
:param secret_key: a secret used to sign the session payload.
|
20
|
+
|
21
|
+
:param max_age: session lifetime in seconds.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, secret_key: str, max_age: int) -> None:
|
25
|
+
self.signer = itsdangerous.TimestampSigner(secret_key)
|
26
|
+
self.max_age = max_age
|
27
|
+
|
28
|
+
def serialize(self, data: Mapping[str, Any]) -> bytes:
|
29
|
+
"""Serialize and sign the session."""
|
30
|
+
dump = json.dumps(data).encode("utf-8")
|
31
|
+
encoded = b64encode(dump)
|
32
|
+
signed = self.signer.sign(encoded)
|
33
|
+
return signed
|
34
|
+
|
35
|
+
def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
|
36
|
+
"""Deserialize the session.
|
37
|
+
|
38
|
+
If the signature is incorect, the session restart from the begining.
|
39
|
+
No exception raised.
|
40
|
+
"""
|
41
|
+
try:
|
42
|
+
data = self.signer.unsign(data, max_age=self.max_age)
|
43
|
+
# We can't deserialize something wrong since the serialize
|
44
|
+
# is signing the content.
|
45
|
+
# If the signature key is compromise and we have invalid payload,
|
46
|
+
# raising exceptions here is fine, it's dangerous afterall.
|
47
|
+
session = json.loads(b64decode(data))
|
48
|
+
except itsdangerous.BadSignature:
|
49
|
+
return {}, True
|
50
|
+
return session, False
|
@@ -42,17 +42,17 @@ class InspectableComponent(Component):
|
|
42
42
|
"""
|
43
43
|
|
44
44
|
__slots__ = (
|
45
|
-
"name",
|
46
|
-
"prefix",
|
47
|
-
"url_prefix",
|
48
|
-
"required",
|
49
|
-
"optional",
|
50
45
|
"css",
|
51
46
|
"js",
|
52
|
-
"path",
|
53
47
|
"mtime",
|
54
|
-
"
|
48
|
+
"name",
|
49
|
+
"optional",
|
50
|
+
"path",
|
51
|
+
"prefix",
|
52
|
+
"required",
|
55
53
|
"source",
|
54
|
+
"tmpl",
|
55
|
+
"url_prefix",
|
56
56
|
)
|
57
57
|
|
58
58
|
def __init__(
|
@@ -14,7 +14,7 @@ from sphinx.roles import XRefRole
|
|
14
14
|
from sphinx.util import relative_uri # type: ignore
|
15
15
|
|
16
16
|
from fastlife.adapters.jinjax.renderer import JinjaxEngine
|
17
|
-
from fastlife.
|
17
|
+
from fastlife.settings import Settings
|
18
18
|
|
19
19
|
|
20
20
|
def create_ref_node(arg_type: str) -> nodes.Node:
|
@@ -4,7 +4,7 @@ Template rending based on JinjaX.
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
import textwrap
|
7
|
-
from collections.abc import
|
7
|
+
from collections.abc import Sequence
|
8
8
|
from typing import (
|
9
9
|
TYPE_CHECKING,
|
10
10
|
Any,
|
@@ -14,15 +14,15 @@ from markupsafe import Markup
|
|
14
14
|
from pydantic.fields import FieldInfo
|
15
15
|
|
16
16
|
from fastlife import Request
|
17
|
+
from fastlife.adapters.fastapi.form import FormModel
|
18
|
+
from fastlife.adapters.fastapi.localizer import get_localizer
|
17
19
|
from fastlife.adapters.jinjax.widget_factory.factory import WidgetFactory
|
18
|
-
from fastlife.
|
19
|
-
from fastlife.request.localizer import get_localizer
|
20
|
-
from fastlife.templates.inline import InlineTemplate
|
20
|
+
from fastlife.domain.model.template import InlineTemplate
|
21
21
|
|
22
22
|
if TYPE_CHECKING:
|
23
|
-
from fastlife.
|
23
|
+
from fastlife.settings import Settings # coverage: ignore
|
24
24
|
|
25
|
-
from fastlife.
|
25
|
+
from fastlife.service.templates import (
|
26
26
|
AbstractTemplateRenderer,
|
27
27
|
AbstractTemplateRendererFactory,
|
28
28
|
)
|
@@ -66,58 +66,10 @@ class JinjaxRenderer(AbstractTemplateRenderer):
|
|
66
66
|
super().__init__(request)
|
67
67
|
self.catalog = catalog
|
68
68
|
self.settings = request.registry.settings
|
69
|
-
self.globals: MutableMapping[str, Any] = {}
|
70
69
|
self.translations = get_localizer(request)
|
70
|
+
self.globals["pydantic_form"] = self.pydantic_form
|
71
71
|
|
72
|
-
def
|
73
|
-
"""
|
74
|
-
Build globals variables accessible in any templates.
|
75
|
-
|
76
|
-
* `request` is the {class}`current request <fastlife.request.request.Request>`
|
77
|
-
* `csrf_token` is used to build for {jinjax:component}`CsrfToken`.
|
78
|
-
"""
|
79
|
-
settings = self.settings
|
80
|
-
ret = {
|
81
|
-
"request": self.request,
|
82
|
-
"csrf_token": {
|
83
|
-
"name": settings.csrf_token_name,
|
84
|
-
"value": self.request.scope.get(settings.csrf_token_name, ""),
|
85
|
-
},
|
86
|
-
"pydantic_form": self.pydantic_form,
|
87
|
-
"localizer": self.translations,
|
88
|
-
**self.globals,
|
89
|
-
}
|
90
|
-
return ret
|
91
|
-
|
92
|
-
def render_template(
|
93
|
-
self,
|
94
|
-
template: str,
|
95
|
-
*,
|
96
|
-
globals: Mapping[str, Any] | None = None,
|
97
|
-
**params: Any,
|
98
|
-
) -> str:
|
99
|
-
"""
|
100
|
-
Render the JinjaX component with the given parameter.
|
101
|
-
|
102
|
-
:param template: the template to render
|
103
|
-
:param globals: parameters that will be used by the JinjaX component and all its
|
104
|
-
child components without "props drilling".
|
105
|
-
:param params: parameters used to render the template.
|
106
|
-
"""
|
107
|
-
|
108
|
-
template = template[: -len(self.settings.jinjax_file_ext) - 1]
|
109
|
-
if globals:
|
110
|
-
self.globals.update(globals)
|
111
|
-
|
112
|
-
self.catalog.jinja_env.install_gettext_translations( # type: ignore
|
113
|
-
self.translations, newstyle=True
|
114
|
-
)
|
115
|
-
|
116
|
-
return self.catalog.render( # type: ignore
|
117
|
-
template, __globals=self.build_globals(), **params
|
118
|
-
)
|
119
|
-
|
120
|
-
def render_inline(self, template: InlineTemplate) -> str:
|
72
|
+
def render_template(self, template: InlineTemplate) -> str:
|
121
73
|
"""
|
122
74
|
Render the JinjaX component with the given parameter.
|
123
75
|
|
@@ -134,7 +86,7 @@ class JinjaxRenderer(AbstractTemplateRenderer):
|
|
134
86
|
return self.catalog.render(
|
135
87
|
template.__class__.__qualname__,
|
136
88
|
__source=src,
|
137
|
-
__globals=self.
|
89
|
+
__globals=self.globals,
|
138
90
|
**params,
|
139
91
|
)
|
140
92
|
|
@@ -28,9 +28,9 @@ class BoolBuilder(BaseWidgetBuilder[bool]):
|
|
28
28
|
) -> BooleanWidget:
|
29
29
|
"""Build the widget."""
|
30
30
|
return BooleanWidget(
|
31
|
-
field_name,
|
31
|
+
name=field_name,
|
32
32
|
removable=removable,
|
33
|
-
title=field.title if field else "",
|
33
|
+
title=field.title or "" if field else "",
|
34
34
|
hint=field.description if field else None,
|
35
35
|
aria_label=(
|
36
36
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -8,9 +8,10 @@ from pydantic.networks import EmailStr
|
|
8
8
|
|
9
9
|
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
10
10
|
from fastlife.adapters.jinjax.widgets.text import TextWidget
|
11
|
+
from fastlife.domain.model.types import Builtins
|
11
12
|
|
12
13
|
|
13
|
-
class EmailStrBuilder(BaseWidgetBuilder[
|
14
|
+
class EmailStrBuilder(BaseWidgetBuilder[Builtins]):
|
14
15
|
"""Builder for Pydantic EmailStr."""
|
15
16
|
|
16
17
|
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
@@ -23,17 +24,17 @@ class EmailStrBuilder(BaseWidgetBuilder[EmailStr]):
|
|
23
24
|
field_name: str,
|
24
25
|
field_type: type[Any],
|
25
26
|
field: FieldInfo | None,
|
26
|
-
value:
|
27
|
+
value: Builtins | None,
|
27
28
|
form_errors: Mapping[str, Any],
|
28
29
|
removable: bool,
|
29
30
|
) -> TextWidget:
|
30
31
|
"""Build the widget."""
|
31
32
|
return TextWidget(
|
32
|
-
field_name,
|
33
|
+
name=field_name,
|
33
34
|
input_type="email",
|
34
35
|
placeholder=str(field.examples[0]) if field and field.examples else None,
|
35
36
|
removable=removable,
|
36
|
-
title=field.title if field else "",
|
37
|
+
title=field.title or "" if field else "",
|
37
38
|
hint=field.description if field else None,
|
38
39
|
aria_label=(
|
39
40
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -31,10 +31,10 @@ class EnumBuilder(BaseWidgetBuilder[Enum]):
|
|
31
31
|
"""Build the widget."""
|
32
32
|
options = [(item.name, item.value) for item in field_type] # type: ignore
|
33
33
|
return DropDownWidget(
|
34
|
-
field_name,
|
34
|
+
name=field_name,
|
35
35
|
options=options, # type: ignore
|
36
36
|
removable=removable,
|
37
|
-
title=field.title if field else "",
|
37
|
+
title=field.title or "" if field else "",
|
38
38
|
hint=field.description if field else None,
|
39
39
|
aria_label=(
|
40
40
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -4,15 +4,17 @@ Create markup for pydantic forms.
|
|
4
4
|
|
5
5
|
import secrets
|
6
6
|
from collections.abc import Mapping
|
7
|
-
from
|
8
|
-
from typing import Any, cast, get_origin
|
7
|
+
from typing import TYPE_CHECKING, Any, get_origin
|
9
8
|
|
10
9
|
from markupsafe import Markup
|
11
10
|
from pydantic.fields import FieldInfo
|
12
11
|
|
13
|
-
from fastlife.adapters.jinjax.widgets.base import Widget
|
14
|
-
from fastlife.
|
15
|
-
from fastlife.
|
12
|
+
from fastlife.adapters.jinjax.widgets.base import CustomWidget, Widget
|
13
|
+
from fastlife.domain.model.form import FormModel
|
14
|
+
from fastlife.domain.model.template import JinjaXTemplate
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
16
18
|
|
17
19
|
from .base import BaseWidgetBuilder
|
18
20
|
from .bool_builder import BoolBuilder
|
@@ -27,6 +29,11 @@ from .simpletype_builder import SimpleTypeBuilder
|
|
27
29
|
from .union_builder import UnionBuilder
|
28
30
|
|
29
31
|
|
32
|
+
class FatalError(JinjaXTemplate):
|
33
|
+
template = """<pydantic_form.FatalError :message="message" />"""
|
34
|
+
message: str
|
35
|
+
|
36
|
+
|
30
37
|
class WidgetFactory:
|
31
38
|
"""
|
32
39
|
Form builder for pydantic model.
|
@@ -35,7 +42,7 @@ class WidgetFactory:
|
|
35
42
|
:param token: reuse a token.
|
36
43
|
"""
|
37
44
|
|
38
|
-
def __init__(self, renderer: AbstractTemplateRenderer, token: str | None = None):
|
45
|
+
def __init__(self, renderer: "AbstractTemplateRenderer", token: str | None = None):
|
39
46
|
self.renderer = renderer
|
40
47
|
self.token = token or secrets.token_urlsafe(4).replace("_", "-")
|
41
48
|
self.builders: list[BaseWidgetBuilder[Any]] = [
|
@@ -70,7 +77,10 @@ class WidgetFactory:
|
|
70
77
|
:param removable: Include a button to remove the model in the markup.
|
71
78
|
:param field: only build the markup of this field is not None.
|
72
79
|
"""
|
73
|
-
|
80
|
+
ret = Markup()
|
81
|
+
if model.fatal_error:
|
82
|
+
ret += self.renderer.render_template(FatalError(message=model.fatal_error))
|
83
|
+
ret += self.get_widget(
|
74
84
|
model.model.__class__,
|
75
85
|
model.form_data,
|
76
86
|
model.errors,
|
@@ -78,6 +88,7 @@ class WidgetFactory:
|
|
78
88
|
removable=removable,
|
79
89
|
field=field,
|
80
90
|
).to_html(self.renderer)
|
91
|
+
return ret
|
81
92
|
|
82
93
|
def get_widget(
|
83
94
|
self,
|
@@ -131,24 +142,22 @@ class WidgetFactory:
|
|
131
142
|
"""
|
132
143
|
if field and field.metadata:
|
133
144
|
for widget in field.metadata:
|
134
|
-
if
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
if field and field.json_schema_extra
|
146
|
-
else None
|
147
|
-
),
|
148
|
-
token=self.token,
|
149
|
-
error=form_errors.get(name),
|
145
|
+
if isinstance(widget, CustomWidget):
|
146
|
+
ret: Widget[Any] = widget.typ(
|
147
|
+
name=name,
|
148
|
+
value=value,
|
149
|
+
removable=removable,
|
150
|
+
title=field.title or "" if field else "",
|
151
|
+
hint=field.description if field else None,
|
152
|
+
aria_label=(
|
153
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
154
|
+
if field and field.json_schema_extra
|
155
|
+
else None
|
150
156
|
),
|
157
|
+
token=self.token,
|
158
|
+
error=form_errors.get(name),
|
151
159
|
)
|
160
|
+
return ret
|
152
161
|
|
153
162
|
type_origin = get_origin(typ)
|
154
163
|
for builder in self.builders:
|
@@ -8,9 +8,10 @@ from pydantic.fields import FieldInfo
|
|
8
8
|
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
9
9
|
from fastlife.adapters.jinjax.widgets.dropdown import DropDownWidget
|
10
10
|
from fastlife.adapters.jinjax.widgets.hidden import HiddenWidget
|
11
|
+
from fastlife.domain.model.types import AnyLiteral
|
11
12
|
|
12
13
|
|
13
|
-
class LiteralBuilder(BaseWidgetBuilder[
|
14
|
+
class LiteralBuilder(BaseWidgetBuilder[AnyLiteral]):
|
14
15
|
"""Builder for Literal."""
|
15
16
|
|
16
17
|
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
@@ -23,7 +24,7 @@ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
|
|
23
24
|
field_name: str,
|
24
25
|
field_type: type[Any], # a literal actually
|
25
26
|
field: FieldInfo | None,
|
26
|
-
value:
|
27
|
+
value: AnyLiteral | None,
|
27
28
|
form_errors: Mapping[str, Any],
|
28
29
|
removable: bool,
|
29
30
|
) -> HiddenWidget | DropDownWidget:
|
@@ -31,15 +32,15 @@ class LiteralBuilder(BaseWidgetBuilder[str]): # str|int|bool
|
|
31
32
|
choices: list[str] = field_type.__args__ # type: ignore
|
32
33
|
if len(choices) == 1:
|
33
34
|
return HiddenWidget(
|
34
|
-
field_name,
|
35
|
+
name=field_name,
|
35
36
|
value=choices[0],
|
36
37
|
token=self.factory.token,
|
37
38
|
)
|
38
39
|
return DropDownWidget(
|
39
|
-
field_name,
|
40
|
-
options=choices,
|
40
|
+
name=field_name,
|
41
|
+
options=choices, # type: ignore
|
41
42
|
removable=removable,
|
42
|
-
title=field.title if field else "",
|
43
|
+
title=field.title or "" if field else "",
|
43
44
|
hint=field.description if field else None,
|
44
45
|
aria_label=(
|
45
46
|
field.json_schema_extra.get("aria_label") # type:ignore
|
@@ -47,11 +47,11 @@ class ModelBuilder(BaseWidgetBuilder[Mapping[str, Any]]):
|
|
47
47
|
form_errors=form_errors,
|
48
48
|
removable=False,
|
49
49
|
)
|
50
|
-
return ModelWidget(
|
51
|
-
field_name,
|
50
|
+
return ModelWidget[Any](
|
51
|
+
name=field_name,
|
52
52
|
value=list(ret.values()),
|
53
53
|
removable=removable,
|
54
|
-
title=field.title if field and field.title else "",
|
54
|
+
title=field.title or "" if field and field.title else "",
|
55
55
|
hint=field.description if field else None,
|
56
56
|
aria_label=(
|
57
57
|
field.json_schema_extra.get("aria_label") # type:ignore
|