fastlifeweb 0.21.0__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 +8 -1
- fastlife/__init__.py +40 -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 +5 -5
- fastlife/adapters/jinjax/widget_factory/factory.py +13 -3
- fastlife/adapters/jinjax/widgets/base.py +1 -1
- fastlife/adapters/jinjax/widgets/model.py +1 -1
- fastlife/adapters/jinjax/widgets/sequence.py +1 -1
- fastlife/adapters/jinjax/widgets/union.py +1 -1
- fastlife/components/Form.jinja +1 -1
- fastlife/components/pydantic_form/FatalError.jinja +8 -0
- fastlife/config/__init__.py +3 -6
- fastlife/config/configurator.py +17 -15
- fastlife/config/resources.py +1 -2
- fastlife/config/views.py +2 -2
- fastlife/domain/model/asgi.py +3 -0
- fastlife/{request → domain/model}/form.py +13 -22
- fastlife/{request → domain/model}/request.py +8 -31
- fastlife/domain/model/security_policy.py +105 -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 +2 -2
- 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 +1 -6
- fastlife/{services → service}/translations.py +3 -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.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
- {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +57 -53
- tailwind.config.js +1 -1
- fastlife/request/__init__.py +0 -5
- fastlife/security/__init__.py +0 -1
- fastlife/security/policy.py +0 -188
- fastlife/templates/__init__.py +0 -7
- fastlife/templates/inline.py +0 -26
- /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
- /fastlife/domain/model/{security.py → csrf.py} +0 -0
- /fastlife/{services → service}/__init__.py +0 -0
- {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
- {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
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
|
+
|
1
8
|
## 0.21.0 - Released on 2024-11-15
|
2
9
|
* Make the InlineTemplate the only way to render views template.
|
3
10
|
* Breaking change: template args is not supported in Configutor.add_route.
|
@@ -51,7 +58,7 @@
|
|
51
58
|
* Hotfix components to create tables
|
52
59
|
|
53
60
|
## 0.15.0 - Released on 2024-09-29
|
54
|
-
* Add an {class}`fastlife.
|
61
|
+
* Add an {class}`fastlife.service.security_policy.AbstractSecurityPolicy` class
|
55
62
|
* New method {meth}`fastlife.config.configurator.GenericConfigurator.set_security_policy`
|
56
63
|
* Breaking change, the check_permission has been removed from the settings.
|
57
64
|
to configure the permission policy, a security policy has to be implemented.
|
fastlife/__init__.py
CHANGED
@@ -5,43 +5,70 @@ __version__ = metadata.version("fastlifeweb")
|
|
5
5
|
from fastapi import Response
|
6
6
|
from fastapi.responses import RedirectResponse
|
7
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
|
8
11
|
from .config import (
|
9
12
|
Configurator,
|
10
|
-
DefaultRegistry,
|
11
13
|
GenericConfigurator,
|
12
|
-
GenericRegistry,
|
13
|
-
Settings,
|
14
14
|
configure,
|
15
|
+
exception_handler,
|
15
16
|
resource,
|
16
17
|
resource_view,
|
17
18
|
view_config,
|
18
19
|
)
|
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
|
+
)
|
19
30
|
from .domain.model.template import JinjaXTemplate
|
20
|
-
from .request import GenericRequest, Registry, Request, get_request
|
21
31
|
|
22
32
|
# from .request.form_data import model
|
23
|
-
from .
|
33
|
+
from .service.registry import DefaultRegistry, GenericRegistry
|
34
|
+
from .service.security_policy import AbstractSecurityPolicy, InsecurePolicy
|
35
|
+
from .settings import Settings
|
24
36
|
|
25
37
|
__all__ = [
|
26
38
|
# Config
|
27
|
-
"configure",
|
28
39
|
"GenericConfigurator",
|
29
|
-
"Configurator",
|
30
|
-
"DefaultRegistry",
|
31
40
|
"GenericRegistry",
|
32
|
-
"
|
41
|
+
"Registry",
|
33
42
|
"Settings",
|
43
|
+
"configure",
|
34
44
|
"view_config",
|
45
|
+
"exception_handler",
|
35
46
|
"resource",
|
36
47
|
"resource_view",
|
37
|
-
|
38
|
-
|
39
|
-
|
48
|
+
"Configurator",
|
49
|
+
"DefaultRegistry",
|
50
|
+
# Form
|
51
|
+
"FormModel",
|
52
|
+
"form_model",
|
53
|
+
# Request
|
40
54
|
"GenericRequest",
|
55
|
+
"AnyRequest",
|
56
|
+
"Request",
|
41
57
|
"get_request",
|
42
|
-
|
58
|
+
# Response
|
43
59
|
"Response",
|
44
60
|
"RedirectResponse",
|
61
|
+
# Security
|
62
|
+
"AbstractSecurityPolicy",
|
63
|
+
"HasPermission",
|
64
|
+
"Unauthenticated",
|
65
|
+
"Allowed",
|
66
|
+
"Denied",
|
67
|
+
"Unauthorized",
|
68
|
+
"Forbidden",
|
69
|
+
"InsecurePolicy",
|
45
70
|
# Template
|
46
71
|
"JinjaXTemplate",
|
72
|
+
# i18n
|
73
|
+
"Localizer",
|
47
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:
|
@@ -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
|
)
|
@@ -10,10 +10,11 @@ from markupsafe import Markup
|
|
10
10
|
from pydantic.fields import FieldInfo
|
11
11
|
|
12
12
|
from fastlife.adapters.jinjax.widgets.base import CustomWidget, Widget
|
13
|
-
from fastlife.
|
13
|
+
from fastlife.domain.model.form import FormModel
|
14
|
+
from fastlife.domain.model.template import JinjaXTemplate
|
14
15
|
|
15
16
|
if TYPE_CHECKING:
|
16
|
-
from fastlife.
|
17
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
17
18
|
|
18
19
|
from .base import BaseWidgetBuilder
|
19
20
|
from .bool_builder import BoolBuilder
|
@@ -28,6 +29,11 @@ from .simpletype_builder import SimpleTypeBuilder
|
|
28
29
|
from .union_builder import UnionBuilder
|
29
30
|
|
30
31
|
|
32
|
+
class FatalError(JinjaXTemplate):
|
33
|
+
template = """<pydantic_form.FatalError :message="message" />"""
|
34
|
+
message: str
|
35
|
+
|
36
|
+
|
31
37
|
class WidgetFactory:
|
32
38
|
"""
|
33
39
|
Form builder for pydantic model.
|
@@ -71,7 +77,10 @@ class WidgetFactory:
|
|
71
77
|
:param removable: Include a button to remove the model in the markup.
|
72
78
|
:param field: only build the markup of this field is not None.
|
73
79
|
"""
|
74
|
-
|
80
|
+
ret = Markup()
|
81
|
+
if model.fatal_error:
|
82
|
+
ret += self.renderer.render_template(FatalError(message=model.fatal_error))
|
83
|
+
ret += self.get_widget(
|
75
84
|
model.model.__class__,
|
76
85
|
model.form_data,
|
77
86
|
model.errors,
|
@@ -79,6 +88,7 @@ class WidgetFactory:
|
|
79
88
|
removable=removable,
|
80
89
|
field=field,
|
81
90
|
).to_html(self.renderer)
|
91
|
+
return ret
|
82
92
|
|
83
93
|
def get_widget(
|
84
94
|
self,
|
@@ -8,7 +8,7 @@ from markupsafe import Markup
|
|
8
8
|
from pydantic import Field, model_validator
|
9
9
|
|
10
10
|
from fastlife.domain.model.template import JinjaXTemplate
|
11
|
-
from fastlife.
|
11
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
12
12
|
from fastlife.shared_utils.infer import is_union
|
13
13
|
|
14
14
|
T = TypeVar("T")
|
@@ -5,7 +5,7 @@ from collections.abc import Sequence
|
|
5
5
|
from markupsafe import Markup
|
6
6
|
from pydantic import Field
|
7
7
|
|
8
|
-
from fastlife.
|
8
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
9
9
|
|
10
10
|
from .base import TWidget, Widget
|
11
11
|
|
@@ -4,7 +4,7 @@ from typing import Any
|
|
4
4
|
from markupsafe import Markup
|
5
5
|
from pydantic import Field
|
6
6
|
|
7
|
-
from fastlife.
|
7
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
8
8
|
|
9
9
|
from .base import TWidget, TypeWrapper, Widget
|
10
10
|
|
@@ -8,7 +8,7 @@ from typing import Union
|
|
8
8
|
from markupsafe import Markup
|
9
9
|
from pydantic import BaseModel, Field
|
10
10
|
|
11
|
-
from fastlife.
|
11
|
+
from fastlife.service.templates import AbstractTemplateRenderer
|
12
12
|
|
13
13
|
from .base import TWidget, TypeWrapper, Widget
|
14
14
|
|
fastlife/components/Form.jinja
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
Create html ``<form>`` node with htmx support by default.
|
3
3
|
A :jinjax:component:`CsrfToken` will always be included in the form
|
4
4
|
and will be checked by the
|
5
|
-
:func:`csrf policy method <fastlife.
|
5
|
+
:func:`csrf policy method <fastlife.service.csrf.check_csrf>`.
|
6
6
|
|
7
7
|
::
|
8
8
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
{# doc display an error for a field. #}
|
2
|
+
{# def message: Annotated[str | None, "error message"] #}
|
3
|
+
{%- if message -%}
|
4
|
+
<div class="flex items-center bg-red-50 border border-red-400 text-red-700" role="alert">
|
5
|
+
<icons.Fire class="m-3 w-16 h-16 fill-orange-500" />
|
6
|
+
<span class="sm:inline text-xl">{{ message }}</span>
|
7
|
+
</div>
|
8
|
+
{%- endif -%}
|
fastlife/config/__init__.py
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
"""Configure fastlife app for dependency injection."""
|
2
2
|
|
3
3
|
from .configurator import Configurator, GenericConfigurator, configure
|
4
|
-
from .
|
4
|
+
from .exceptions import exception_handler
|
5
5
|
from .resources import resource, resource_view
|
6
|
-
from .settings import Settings
|
7
6
|
from .views import view_config
|
8
7
|
|
9
8
|
__all__ = [
|
10
9
|
"Configurator",
|
11
10
|
"GenericConfigurator",
|
12
11
|
"configure",
|
13
|
-
"
|
12
|
+
"exception_handler",
|
14
13
|
"resource",
|
15
14
|
"resource_view",
|
16
|
-
"
|
17
|
-
"DefaultRegistry",
|
18
|
-
"Settings",
|
15
|
+
"view_config",
|
19
16
|
]
|
fastlife/config/configurator.py
CHANGED
@@ -27,27 +27,29 @@ from fastapi.params import Depends as DependsType
|
|
27
27
|
from fastapi.staticfiles import StaticFiles
|
28
28
|
from fastapi.types import IncEx
|
29
29
|
|
30
|
+
from fastlife.adapters.fastapi.request import GenericRequest, Request
|
31
|
+
from fastlife.adapters.fastapi.routing.route import Route
|
32
|
+
from fastlife.adapters.fastapi.routing.router import Router
|
30
33
|
from fastlife.config.openapiextra import OpenApiTag
|
34
|
+
from fastlife.domain.model.template import InlineTemplate
|
31
35
|
from fastlife.middlewares.base import AbstractMiddleware
|
32
|
-
from fastlife.
|
33
|
-
from fastlife.
|
34
|
-
from fastlife.
|
35
|
-
from fastlife.
|
36
|
-
from fastlife.
|
37
|
-
from fastlife.shared_utils.resolver import
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
from .settings import Settings
|
36
|
+
from fastlife.service.check_permission import check_permission
|
37
|
+
from fastlife.service.csrf import check_csrf
|
38
|
+
from fastlife.service.registry import DefaultRegistry, TRegistry
|
39
|
+
from fastlife.settings import Settings
|
40
|
+
from fastlife.shared_utils.infer import is_inline_template_returned
|
41
|
+
from fastlife.shared_utils.resolver import (
|
42
|
+
resolve,
|
43
|
+
resolve_maybe_relative,
|
44
|
+
)
|
42
45
|
|
43
46
|
if TYPE_CHECKING:
|
44
|
-
from fastlife.
|
45
|
-
from fastlife.
|
47
|
+
from fastlife.service.security_policy import AbstractSecurityPolicy
|
48
|
+
from fastlife.service.templates import (
|
46
49
|
AbstractTemplateRendererFactory, # coverage: ignore
|
47
50
|
)
|
48
|
-
from fastlife.templates.inline import InlineTemplate
|
49
51
|
|
50
|
-
from fastlife.
|
52
|
+
from fastlife.service.locale_negociator import LocaleNegociator
|
51
53
|
|
52
54
|
log = logging.getLogger(__name__)
|
53
55
|
VENUSIAN_CATEGORY = "fastlife"
|
@@ -519,7 +521,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
519
521
|
parameters.
|
520
522
|
:param endpoint: the function that will reveive the request.
|
521
523
|
:param permission: a permission to validate by the
|
522
|
-
{class}`Security Policy <fastlife.
|
524
|
+
{class}`Security Policy <fastlife.service.security_policy.AbstractSecurityPolicy>`.
|
523
525
|
:param status_code: customize response status code.
|
524
526
|
:param methods: restrict route to a list of http methods.
|
525
527
|
:return: the configurator.
|
fastlife/config/resources.py
CHANGED
@@ -170,8 +170,7 @@ def resource_view(
|
|
170
170
|
`collection_head`, `collection_options`, `get`, `post`, `put`, `patch`, `delete`,
|
171
171
|
`head` or `options`.
|
172
172
|
|
173
|
-
:param permission: a permission to validate by the
|
174
|
-
:attr:`fastlife.config.settings.Settings.check_permission` function.
|
173
|
+
:param permission: a permission to validate by the security policy.
|
175
174
|
:param status_code: returned status_code
|
176
175
|
:param summary: OpenAPI summary for the route.
|
177
176
|
:param description:OpenAPI description for the route.
|
fastlife/config/views.py
CHANGED
@@ -45,9 +45,9 @@ def view_config(
|
|
45
45
|
:param path: path of the route, use `{curly_brace}` to inject FastAPI Path
|
46
46
|
parameters.
|
47
47
|
:param template: the template rendered by the
|
48
|
-
{class}`fastlife.
|
48
|
+
{class}`fastlife.service.templates.AbstractTemplateRenderer`.
|
49
49
|
:param permission: a permission to validate by the
|
50
|
-
{class}`Security Policy <fastlife.
|
50
|
+
{class}`Security Policy <fastlife.service.security_policy.AbstractSecurityPolicy>`.
|
51
51
|
:param status_code: customize response status code.
|
52
52
|
:param methods: restrict route to a list of http methods.
|
53
53
|
|
@@ -1,13 +1,10 @@
|
|
1
1
|
"""HTTP Form serialization."""
|
2
2
|
|
3
|
-
from collections.abc import
|
3
|
+
from collections.abc import Mapping
|
4
4
|
from typing import Any, Generic, TypeVar, get_origin
|
5
5
|
|
6
|
-
from fastapi import Depends
|
7
6
|
from pydantic import BaseModel, ValidationError
|
8
7
|
|
9
|
-
from fastlife import Registry
|
10
|
-
from fastlife.request.form_data import MappingFormData
|
11
8
|
from fastlife.shared_utils.infer import is_union
|
12
9
|
|
13
10
|
T = TypeVar("T", bound=BaseModel)
|
@@ -17,14 +14,16 @@ T = TypeVar("T", bound=BaseModel)
|
|
17
14
|
class FormModel(Generic[T]):
|
18
15
|
prefix: str
|
19
16
|
model: T
|
20
|
-
|
17
|
+
fatal_error: str
|
18
|
+
errors: dict[str, str]
|
21
19
|
is_valid: bool
|
22
20
|
|
23
21
|
def __init__(
|
24
|
-
self, prefix: str, model: T, errors:
|
22
|
+
self, prefix: str, model: T, errors: dict[str, Any], is_valid: bool = False
|
25
23
|
) -> None:
|
26
24
|
self.prefix = prefix
|
27
25
|
self.model = model
|
26
|
+
self.fatal_error = ""
|
28
27
|
self.errors = errors
|
29
28
|
self.is_valid = is_valid
|
30
29
|
|
@@ -32,6 +31,14 @@ class FormModel(Generic[T]):
|
|
32
31
|
def default(cls, prefix: str, pydantic_type: type[T]) -> "FormModel[T]":
|
33
32
|
return cls(prefix, pydantic_type.model_construct(), {})
|
34
33
|
|
34
|
+
def set_fatal_error(self, value: str) -> None:
|
35
|
+
self.fatal_error = value
|
36
|
+
self.is_valid = False
|
37
|
+
|
38
|
+
def add_error(self, field: str, value: str) -> None:
|
39
|
+
self.errors[f"{self.prefix}.{field}"] = value
|
40
|
+
self.is_valid = False
|
41
|
+
|
35
42
|
def edit(self, pydantic_type: T) -> None:
|
36
43
|
"""
|
37
44
|
Load the form with the given model and consider it as valid for the user.
|
@@ -86,19 +93,3 @@ class FormModel(Generic[T]):
|
|
86
93
|
errors[loc] = error["msg"]
|
87
94
|
model = pydantic_type.model_construct(**data.get(prefix, {}))
|
88
95
|
return cls(prefix, model, errors)
|
89
|
-
|
90
|
-
|
91
|
-
def form_model(
|
92
|
-
cls: type[T], name: str | None = None
|
93
|
-
) -> Callable[[Mapping[str, Any]], FormModel[T]]:
|
94
|
-
"""
|
95
|
-
Build a model, a class of type T based on Pydandic Base Model from a form payload.
|
96
|
-
"""
|
97
|
-
|
98
|
-
def to_model(data: MappingFormData, registry: Registry) -> FormModel[T]:
|
99
|
-
prefix = name or registry.settings.form_data_model_prefix
|
100
|
-
if not data:
|
101
|
-
return FormModel[T].default(prefix, cls)
|
102
|
-
return FormModel[T].from_payload(prefix, cls, data)
|
103
|
-
|
104
|
-
return Depends(to_model)
|