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.
Files changed (62) hide show
  1. CHANGELOG.md +8 -1
  2. fastlife/__init__.py +40 -13
  3. fastlife/adapters/__init__.py +1 -1
  4. fastlife/adapters/fastapi/__init__.py +9 -0
  5. fastlife/adapters/fastapi/form.py +26 -0
  6. fastlife/{request → adapters/fastapi}/form_data.py +1 -1
  7. fastlife/{request → adapters/fastapi}/localizer.py +4 -2
  8. fastlife/adapters/fastapi/request.py +33 -0
  9. fastlife/{routing → adapters/fastapi/routing}/route.py +3 -3
  10. fastlife/{routing → adapters/fastapi/routing}/router.py +1 -1
  11. fastlife/adapters/itsdangerous/__init__.py +3 -0
  12. fastlife/adapters/itsdangerous/session.py +50 -0
  13. fastlife/adapters/jinjax/jinjax_ext/inspectable_component.py +7 -7
  14. fastlife/adapters/jinjax/jinjax_ext/jinjax_doc.py +1 -1
  15. fastlife/adapters/jinjax/renderer.py +5 -5
  16. fastlife/adapters/jinjax/widget_factory/factory.py +13 -3
  17. fastlife/adapters/jinjax/widgets/base.py +1 -1
  18. fastlife/adapters/jinjax/widgets/model.py +1 -1
  19. fastlife/adapters/jinjax/widgets/sequence.py +1 -1
  20. fastlife/adapters/jinjax/widgets/union.py +1 -1
  21. fastlife/components/Form.jinja +1 -1
  22. fastlife/components/pydantic_form/FatalError.jinja +8 -0
  23. fastlife/config/__init__.py +3 -6
  24. fastlife/config/configurator.py +17 -15
  25. fastlife/config/resources.py +1 -2
  26. fastlife/config/views.py +2 -2
  27. fastlife/domain/model/asgi.py +3 -0
  28. fastlife/{request → domain/model}/form.py +13 -22
  29. fastlife/{request → domain/model}/request.py +8 -31
  30. fastlife/domain/model/security_policy.py +105 -0
  31. fastlife/middlewares/base.py +1 -1
  32. fastlife/middlewares/reverse_proxy/x_forwarded.py +1 -2
  33. fastlife/middlewares/session/__init__.py +2 -2
  34. fastlife/middlewares/session/middleware.py +4 -3
  35. fastlife/middlewares/session/serializer.py +0 -44
  36. fastlife/{services/policy.py → service/check_permission.py} +1 -1
  37. fastlife/{security → service}/csrf.py +2 -2
  38. fastlife/{services → service}/locale_negociator.py +5 -8
  39. fastlife/{config → service}/registry.py +13 -7
  40. fastlife/service/security_policy.py +100 -0
  41. fastlife/{services → service}/templates.py +1 -6
  42. fastlife/{services → service}/translations.py +3 -0
  43. fastlife/{config/settings.py → settings.py} +6 -12
  44. fastlife/shared_utils/infer.py +24 -1
  45. fastlife/{templates/constants.py → template_globals.py} +2 -2
  46. fastlife/testing/testclient.py +2 -2
  47. fastlife/views/__init__.py +1 -0
  48. fastlife/views/pydantic_form.py +6 -0
  49. {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/METADATA +1 -1
  50. {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/RECORD +57 -53
  51. tailwind.config.js +1 -1
  52. fastlife/request/__init__.py +0 -5
  53. fastlife/security/__init__.py +0 -1
  54. fastlife/security/policy.py +0 -188
  55. fastlife/templates/__init__.py +0 -7
  56. fastlife/templates/inline.py +0 -26
  57. /fastlife/{routing → adapters/fastapi/routing}/__init__.py +0 -0
  58. /fastlife/domain/model/{security.py → csrf.py} +0 -0
  59. /fastlife/{services → service}/__init__.py +0 -0
  60. {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/WHEEL +0 -0
  61. {fastlifeweb-0.21.0.dist-info → fastlifeweb-0.22.0.dist-info}/entry_points.txt +0 -0
  62. {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.security.policy.AbstractSecurityPolicy` class
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 .services.templates import TemplateParams
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
- "TemplateParams",
41
+ "Registry",
33
42
  "Settings",
43
+ "configure",
34
44
  "view_config",
45
+ "exception_handler",
35
46
  "resource",
36
47
  "resource_view",
37
- # Model
38
- # "model",
39
- "Request",
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
- "Registry",
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
  ]
@@ -1,3 +1,3 @@
1
1
  """
2
- Adapters are implementations of abstract classed defined in {mod}`fastlife.services`.
2
+ Adapters are implementations of abstract classed defined in {mod}`fastlife.service`.
3
3
  """
@@ -0,0 +1,9 @@
1
+ """
2
+ Adapters based on top of FastAPI.
3
+
4
+ At the momdent, the code is highly coupled with FastAPI,
5
+ some part of the code import fastapi directly for legacy reason,
6
+ but it shouldn't.
7
+
8
+ All FastAPI dependency should be contained in this module.
9
+ """
@@ -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)
@@ -10,7 +10,7 @@ from typing import (
10
10
 
11
11
  from fastapi import Depends
12
12
 
13
- from fastlife import Request
13
+ from fastlife.adapters.fastapi.request import Request
14
14
 
15
15
 
16
16
  def unflatten_struct(
@@ -2,12 +2,14 @@ from typing import Annotated
2
2
 
3
3
  from fastapi import Depends
4
4
 
5
- from fastlife.request.request import Request
6
- from fastlife.services.translations import Localizer as RequestLocalizer
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.request.request import Request
10
+ from fastlife.domain.model.request import GenericRequest
11
11
 
12
12
  if TYPE_CHECKING:
13
- from fastlife.config.registry import DefaultRegistry # coverage: ignore
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 = Request(self._registry, request)
44
+ req = GenericRequest(self._registry, request)
45
45
  return await orig_route_handler(req)
46
46
 
47
47
  return route_handler
@@ -9,7 +9,7 @@ from typing import Any
9
9
 
10
10
  from fastapi import APIRouter
11
11
 
12
- from fastlife.routing.route import Route
12
+ from fastlife.adapters.fastapi.routing.route import Route
13
13
 
14
14
 
15
15
  class Router(APIRouter):
@@ -0,0 +1,3 @@
1
+ from .session import SignedSessionSerializer
2
+
3
+ __all__ = ["SignedSessionSerializer"]
@@ -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
- "tmpl",
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.config.settings import Settings
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.request.form import FormModel
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.config.settings import Settings # coverage: ignore
23
+ from fastlife.settings import Settings # coverage: ignore
24
24
 
25
- from fastlife.services.templates import (
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.request.form import FormModel
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.services.templates import AbstractTemplateRenderer
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
- return self.get_widget(
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.services.templates import AbstractTemplateRenderer
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.services.templates import AbstractTemplateRenderer
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.services.templates import AbstractTemplateRenderer
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.services.templates import AbstractTemplateRenderer
11
+ from fastlife.service.templates import AbstractTemplateRenderer
12
12
 
13
13
  from .base import TWidget, TypeWrapper, Widget
14
14
 
@@ -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.security.csrf.check_csrf>`.
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 -%}
@@ -1,19 +1,16 @@
1
1
  """Configure fastlife app for dependency injection."""
2
2
 
3
3
  from .configurator import Configurator, GenericConfigurator, configure
4
- from .registry import DefaultRegistry, GenericRegistry
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
- "view_config",
12
+ "exception_handler",
14
13
  "resource",
15
14
  "resource_view",
16
- "GenericRegistry",
17
- "DefaultRegistry",
18
- "Settings",
15
+ "view_config",
19
16
  ]
@@ -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.request.request import GenericRequest, Request
33
- from fastlife.routing.route import Route
34
- from fastlife.routing.router import Router
35
- from fastlife.security.csrf import check_csrf
36
- from fastlife.services.policy import check_permission
37
- from fastlife.shared_utils.resolver import resolve, resolve_maybe_relative
38
- from fastlife.templates.inline import is_inline_template_returned
39
-
40
- from .registry import DefaultRegistry, TRegistry
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.security.policy import AbstractSecurityPolicy # coverage: ignore
45
- from fastlife.services.templates import (
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.services.locale_negociator import LocaleNegociator
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.security.policy.AbstractSecurityPolicy>`.
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.
@@ -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.services.templates.AbstractTemplateRenderer`.
48
+ {class}`fastlife.service.templates.AbstractTemplateRenderer`.
49
49
  :param permission: a permission to validate by the
50
- {class}`Security Policy <fastlife.security.policy.AbstractSecurityPolicy>`.
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
 
@@ -0,0 +1,3 @@
1
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
2
+
3
+ __all__ = ["ASGIApp", "Message", "Receive", "Scope", "Send"]
@@ -1,13 +1,10 @@
1
1
  """HTTP Form serialization."""
2
2
 
3
- from collections.abc import Callable, Mapping
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
- errors: Mapping[str, str]
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: Mapping[str, Any], is_valid: bool = False
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)