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
@@ -1,21 +1,20 @@
1
1
  """HTTP Request representation in a python object."""
2
2
 
3
- from typing import TYPE_CHECKING, Annotated, Any, Generic
3
+ from typing import TYPE_CHECKING, Any, Generic
4
4
 
5
- from fastapi import Request as FastAPIRequest
6
- from fastapi.params import Depends
5
+ from starlette.requests import Request as BaseRequest
7
6
 
8
- from fastlife.config.registry import DefaultRegistry, TRegistry
9
- from fastlife.domain.model.security import CSRFToken, create_csrf_token
7
+ from fastlife.domain.model.csrf import CSRFToken, create_csrf_token
8
+ from fastlife.service.registry import TRegistry
10
9
 
11
10
  if TYPE_CHECKING:
12
- from fastlife.security.policy import ( # coverage: ignore
11
+ from fastlife.service.security_policy import ( # coverage: ignore
13
12
  AbstractSecurityPolicy,
14
13
  HasPermission,
15
14
  )
16
15
 
17
16
 
18
- class GenericRequest(FastAPIRequest, Generic[TRegistry]):
17
+ class GenericRequest(BaseRequest, Generic[TRegistry]):
19
18
  """HTTP Request representation."""
20
19
 
21
20
  registry: TRegistry
@@ -28,7 +27,7 @@ class GenericRequest(FastAPIRequest, Generic[TRegistry]):
28
27
 
29
28
  renderer_globals: dict[str, Any]
30
29
 
31
- def __init__(self, registry: TRegistry, request: FastAPIRequest) -> None:
30
+ def __init__(self, registry: TRegistry, request: BaseRequest) -> None:
32
31
  super().__init__(request.scope, request.receive)
33
32
  self.registry = registry
34
33
  self.locale_name = registry.locale_negociator(self)
@@ -64,29 +63,7 @@ class GenericRequest(FastAPIRequest, Generic[TRegistry]):
64
63
  if self.security_policy is None:
65
64
  raise RuntimeError(
66
65
  f"Request {self.url.path} require a security policy, "
67
- "explicit fastlife.security.policy.InsecurePolicy is required."
66
+ "explicit fastlife.service.security_policy.InsecurePolicy is required."
68
67
  )
69
68
 
70
69
  return await self.security_policy.has_permission(permission)
71
-
72
-
73
- def get_request(request: FastAPIRequest) -> GenericRequest[Any]:
74
- return request # type: ignore
75
-
76
-
77
- Request = Annotated[GenericRequest[DefaultRegistry], Depends(get_request)]
78
- """A request that is associated to the default registry."""
79
- # FastAPI handle its Request objects using a lenient_issubclass,
80
- # basically a issubclass(Request), doe to the Generic[T], it does not work.
81
-
82
-
83
- AnyRequest = Annotated[GenericRequest[Any], Depends(get_request)]
84
- """A request version that is associated to the any registry."""
85
-
86
-
87
- def get_registry(request: Request) -> DefaultRegistry:
88
- return request.registry
89
-
90
-
91
- Registry = Annotated[DefaultRegistry, Depends(get_registry)]
92
- """FastAPI dependency to access to the registry."""
@@ -0,0 +1,105 @@
1
+ """Security policy."""
2
+
3
+ import logging
4
+ from collections.abc import Callable, Coroutine
5
+ from typing import Any, Literal, TypeVar
6
+
7
+ from fastapi import HTTPException
8
+ from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
9
+
10
+ CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
11
+ CheckPermission = Callable[[str], CheckPermissionHook]
12
+
13
+ TUser = TypeVar("TUser")
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class Unauthorized(HTTPException):
19
+ """An exception raised to stop a request exectution and return an HTTP Error."""
20
+
21
+ def __init__(
22
+ self,
23
+ status_code: int = HTTP_401_UNAUTHORIZED,
24
+ detail: str = "Unauthorized",
25
+ headers: dict[str, str] | None = None,
26
+ ) -> None:
27
+ super().__init__(status_code, detail, headers)
28
+
29
+
30
+ class Forbidden(HTTPException):
31
+ """An exception raised to stop a request exectution and return an HTTP Error."""
32
+
33
+ def __init__(
34
+ self,
35
+ status_code: int = HTTP_403_FORBIDDEN,
36
+ detail: str = "Forbidden",
37
+ headers: dict[str, str] | None = None,
38
+ ) -> None:
39
+ super().__init__(status_code, detail, headers)
40
+
41
+
42
+ class BoolMeta(type):
43
+ def __bool__(cls) -> bool:
44
+ return cls.kind == "allowed" # type: ignore
45
+
46
+ def __repr__(cls) -> str:
47
+ return cls.reason # type: ignore
48
+
49
+
50
+ class HasPermission(int, metaclass=BoolMeta):
51
+ """
52
+ A type used to know if a permission is allowed or not.
53
+
54
+ It behave has a boolean, but 3 possibilities exists defind has 3 sub-types
55
+ {class}`Allowed` {class}`Unauthenticated` or {class}`Denied`.
56
+
57
+ In many cases Unauthenticated call may redirect to a login page,
58
+ where authenticated user are not redirected. they have an error message,
59
+ or the frontend may use the information to adapt its interface.
60
+ """
61
+
62
+ kind: Literal["allowed", "unauthenticated", "denied"]
63
+ """
64
+ Identified basic information of the response.
65
+ It distinguished unauthenticated and denied to eventually raised 401 over 403 error.
66
+ """
67
+ reason: str
68
+ """A human explanation of the response."""
69
+
70
+ def __new__(cls, reason: str) -> "HasPermission":
71
+ instance = super().__new__(cls)
72
+ instance.reason = reason
73
+ return instance
74
+
75
+ def __repr__(self) -> str:
76
+ return self.reason
77
+
78
+ def __bool__(self) -> bool:
79
+ return self.kind == "allowed"
80
+
81
+
82
+ class Allowed(HasPermission):
83
+ """Represent a permission check result that is allowed."""
84
+
85
+ kind = "allowed"
86
+ reason = "Allowed"
87
+
88
+
89
+ class Unauthenticated(HasPermission):
90
+ """
91
+ Represent a permission check result that is not allowed due to
92
+ missing authentication mechanism.
93
+ """
94
+
95
+ kind = "unauthenticated"
96
+ reason = "Authentication required"
97
+
98
+
99
+ class Denied(HasPermission):
100
+ """
101
+ Represent a permission check result that is not allowed due to lack of permission.
102
+ """
103
+
104
+ kind = "denied"
105
+ reason = "Access denied to this resource"
@@ -2,7 +2,7 @@
2
2
 
3
3
  import abc
4
4
 
5
- from starlette.types import Receive, Scope, Send
5
+ from fastlife.domain.model.asgi import Receive, Scope, Send
6
6
 
7
7
 
8
8
  class AbstractMiddleware(abc.ABC):
@@ -1,8 +1,7 @@
1
1
  import logging
2
2
  from collections.abc import Sequence
3
3
 
4
- from starlette.types import ASGIApp, Receive, Scope, Send
5
-
4
+ from fastlife.domain.model.asgi import ASGIApp, Receive, Scope, Send
6
5
  from fastlife.middlewares.base import AbstractMiddleware
7
6
 
8
7
  log = logging.getLogger(__name__)
@@ -15,9 +15,9 @@ from fastlife import Configurator, configure
15
15
  from fastlife.shared_utils.resolver import resolve
16
16
 
17
17
  from .middleware import SessionMiddleware
18
- from .serializer import AbsractSessionSerializer, SignedSessionSerializer
18
+ from .serializer import AbsractSessionSerializer
19
19
 
20
- __all__ = ["SessionMiddleware", "AbsractSessionSerializer", "SignedSessionSerializer"]
20
+ __all__ = ["AbsractSessionSerializer", "SessionMiddleware"]
21
21
 
22
22
 
23
23
  @configure
@@ -5,11 +5,11 @@ from typing import Literal
5
5
 
6
6
  from starlette.datastructures import MutableHeaders
7
7
  from starlette.requests import HTTPConnection
8
- from starlette.types import ASGIApp, Message, Receive, Scope, Send
9
8
 
9
+ from fastlife.domain.model.asgi import ASGIApp, Message, Receive, Scope, Send
10
10
  from fastlife.middlewares.base import AbstractMiddleware
11
11
 
12
- from .serializer import AbsractSessionSerializer, SignedSessionSerializer
12
+ from .serializer import AbsractSessionSerializer
13
13
 
14
14
 
15
15
  class SessionMiddleware(AbstractMiddleware):
@@ -17,6 +17,7 @@ class SessionMiddleware(AbstractMiddleware):
17
17
 
18
18
  def __init__(
19
19
  self,
20
+ *,
20
21
  app: ASGIApp,
21
22
  cookie_name: str,
22
23
  secret_key: str,
@@ -25,7 +26,7 @@ class SessionMiddleware(AbstractMiddleware):
25
26
  cookie_same_site: Literal["lax", "strict", "none"] = "lax",
26
27
  cookie_secure: bool = False,
27
28
  cookie_domain: str = "",
28
- serializer: type[AbsractSessionSerializer] = SignedSessionSerializer,
29
+ serializer: type[AbsractSessionSerializer],
29
30
  ) -> None:
30
31
  self.app = app
31
32
  self.max_age = int(duration.total_seconds())
@@ -1,13 +1,9 @@
1
1
  """Serialize session."""
2
2
 
3
3
  import abc
4
- import json
5
- from base64 import b64decode, b64encode
6
4
  from collections.abc import Mapping
7
5
  from typing import Any
8
6
 
9
- import itsdangerous
10
-
11
7
 
12
8
  class AbsractSessionSerializer(abc.ABC):
13
9
  """Session serializer base class"""
@@ -24,43 +20,3 @@ class AbsractSessionSerializer(abc.ABC):
24
20
  def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
25
21
  """Derialize the session raw bytes content and return it as a mapping."""
26
22
  ...
27
-
28
-
29
- class SignedSessionSerializer(AbsractSessionSerializer):
30
- """
31
- The default fastlife session serializer.
32
-
33
- It's based on the itsdangerous package to sign the session with a secret key.
34
-
35
- :param secret_key: a secret used to sign the session payload.
36
-
37
- :param max_age: session lifetime in seconds.
38
- """
39
-
40
- def __init__(self, secret_key: str, max_age: int) -> None:
41
- self.signer = itsdangerous.TimestampSigner(secret_key)
42
- self.max_age = max_age
43
-
44
- def serialize(self, data: Mapping[str, Any]) -> bytes:
45
- """Serialize and sign the session."""
46
- dump = json.dumps(data).encode("utf-8")
47
- encoded = b64encode(dump)
48
- signed = self.signer.sign(encoded)
49
- return signed
50
-
51
- def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
52
- """Deserialize the session.
53
-
54
- If the signature is incorect, the session restart from the begining.
55
- No exception raised.
56
- """
57
- try:
58
- data = self.signer.unsign(data, max_age=self.max_age)
59
- # We can't deserialize something wrong since the serialize
60
- # is signing the content.
61
- # If the signature key is compromise and we have invalid payload,
62
- # raising exceptions here is fine, it's dangerous afterall.
63
- session = json.loads(b64decode(data))
64
- except itsdangerous.BadSignature:
65
- return {}, True
66
- return session, False
@@ -26,7 +26,7 @@ def check_permission(permission_name: str) -> CheckPermissionHook:
26
26
  if request.security_policy is None:
27
27
  raise RuntimeError(
28
28
  f"Request {request.url.path} require a security policy, "
29
- "explicit fastlife.security.policy.InsecurePolicy is required"
29
+ "explicit fastlife.service.security_policy.InsecurePolicy is required"
30
30
  )
31
31
  allowed = await request.security_policy.has_permission(permission_name)
32
32
  match allowed.kind:
@@ -6,7 +6,7 @@ Fast life did not reinvent the wheel on CSRF Protection.
6
6
  It use the good old method. A CSRF token is saved in a cookie.
7
7
  Forms post the CSRF token, and the token in the cookies and the form must match
8
8
  to process the request, otherwise an exception
9
- {class}`fastlife.security.csrf.CSRFAttack` is raised.
9
+ {class}`fastlife.service.csrf.CSRFAttack` is raised.
10
10
 
11
11
  The cookie named is configurabllefia the settings
12
12
  :attr:`fastlife.config.settings.Settings.csrf_token_name`
@@ -21,7 +21,7 @@ no way to prevent to set the cookie in the request.
21
21
  from collections.abc import Callable, Coroutine
22
22
  from typing import Any
23
23
 
24
- from fastlife.request import Request
24
+ from fastlife.adapters.fastapi.request import Request
25
25
 
26
26
 
27
27
  class CSRFAttack(Exception):
@@ -1,20 +1,17 @@
1
1
  """Find the localization gor the given request."""
2
2
 
3
3
  from collections.abc import Callable
4
- from typing import TYPE_CHECKING, Any
4
+ from typing import Any
5
5
 
6
- from fastlife.config.settings import Settings
6
+ from fastlife.settings import Settings
7
7
 
8
8
  LocaleName = str
9
9
  """The LocaleName is a locale such as en, fr that will be consume for translations."""
10
10
 
11
- if TYPE_CHECKING:
12
- from fastlife.request.request import GenericRequest # coverage: ignore
11
+ from fastlife.adapters.fastapi.request import GenericRequest # coverage: ignore
13
12
 
14
- LocaleNegociator = Callable[[GenericRequest[Any]], LocaleName] # coverage: ignore
15
- """Interface to implement to negociate a locale""" # coverage: ignore
16
- else:
17
- LocaleNegociator = Any
13
+ LocaleNegociator = Callable[[GenericRequest[Any]], LocaleName] # coverage: ignore
14
+ """Interface to implement to negociate a locale""" # coverage: ignore
18
15
 
19
16
 
20
17
  def default_negociator(settings: Settings) -> LocaleNegociator:
@@ -1,15 +1,14 @@
1
1
  from collections.abc import Mapping
2
2
  from typing import TYPE_CHECKING, Generic, TypeVar
3
3
 
4
- from fastlife.services.locale_negociator import LocaleNegociator, default_negociator
5
- from fastlife.services.translations import LocalizerFactory
6
-
7
4
  if TYPE_CHECKING:
8
- from fastlife.services.templates import ( # coverage: ignore
5
+ from fastlife.service.locale_negociator import LocaleNegociator # coverage: ignore
6
+ from fastlife.service.templates import ( # coverage: ignore
9
7
  AbstractTemplateRendererFactory, # coverage: ignore
10
8
  ) # coverage: ignore
9
+ from fastlife.service.translations import LocalizerFactory # coverage: ignore
11
10
 
12
- from .settings import Settings
11
+ from fastlife.settings import Settings
13
12
 
14
13
  TSettings = TypeVar("TSettings", bound=Settings, covariant=True)
15
14
  """
@@ -24,11 +23,18 @@ class GenericRegistry(Generic[TSettings]):
24
23
  """
25
24
 
26
25
  settings: Settings
26
+ """Application settings."""
27
27
  renderers: Mapping[str, "AbstractTemplateRendererFactory"]
28
- locale_negociator: LocaleNegociator
29
- localizer: LocalizerFactory
28
+ """Registered template engine."""
29
+ locale_negociator: "LocaleNegociator"
30
+ """Used to fine the best language for the response."""
31
+ localizer: "LocalizerFactory"
32
+ """Used to localized message."""
30
33
 
31
34
  def __init__(self, settings: Settings) -> None:
35
+ from fastlife.service.locale_negociator import default_negociator
36
+ from fastlife.service.translations import LocalizerFactory
37
+
32
38
  self.settings = settings
33
39
  self.locale_negociator = default_negociator(self.settings)
34
40
  self.renderers = {}
@@ -0,0 +1,100 @@
1
+ """Security policy."""
2
+
3
+ import abc
4
+ from typing import Annotated, Any, Generic
5
+ from uuid import UUID
6
+
7
+ from fastapi import Depends
8
+
9
+ from fastlife import GenericRequest, get_request
10
+ from fastlife.domain.model.security_policy import (
11
+ Allowed,
12
+ Forbidden,
13
+ HasPermission,
14
+ TUser,
15
+ Unauthorized,
16
+ )
17
+ from fastlife.service.registry import TRegistry
18
+
19
+
20
+ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]):
21
+ """Security policy base class."""
22
+
23
+ Forbidden = Forbidden
24
+ """The exception raised if the user identified is not granted."""
25
+ Unauthorized = Unauthorized
26
+ """The exception raised if no user has been identified."""
27
+
28
+ request: GenericRequest[TRegistry]
29
+ """Request where the security policy is applied."""
30
+
31
+ def __init__(
32
+ self, request: Annotated[GenericRequest[TRegistry], Depends(get_request)]
33
+ ):
34
+ """
35
+ Build the security policy.
36
+
37
+ When implementing a security policy, multiple parameters can be added
38
+ to the constructor as FastAPI dependencies, using the `Depends` FastAPI
39
+ annotation.
40
+ The security policy is installed has a depenency of the router that hold
41
+ a route prefix of the application.
42
+ """
43
+ self.request = request
44
+ self.request.security_policy = self # we do backref to implement has_permission
45
+
46
+ @abc.abstractmethod
47
+ async def identity(self) -> TUser | None:
48
+ """
49
+ Return app-specific user object or raise an HTTPException.
50
+ """
51
+
52
+ @abc.abstractmethod
53
+ async def authenticated_userid(self) -> str | UUID | None:
54
+ """
55
+ Return app-specific user object or raise an HTTPException.
56
+ """
57
+
58
+ @abc.abstractmethod
59
+ async def has_permission(
60
+ self, permission: str
61
+ ) -> HasPermission | type[HasPermission]:
62
+ """Allow access to everything if signed in."""
63
+
64
+ @abc.abstractmethod
65
+ async def remember(self, user: TUser) -> None:
66
+ """Save the user identity in the request session."""
67
+
68
+ @abc.abstractmethod
69
+ async def forget(self) -> None:
70
+ """Destroy the request session."""
71
+
72
+
73
+ class InsecurePolicy(AbstractSecurityPolicy[None, Any]):
74
+ """
75
+ An implementation of the security policy made for explicit unsecured access.
76
+
77
+ Setting a permission on a view require a security policy, if not set, accessing
78
+ to a view will raise a RuntimeError. To bypass this error for testing purpose
79
+ or your own reason, the InsecurePolicy has to be set to the configurator.
80
+ """
81
+
82
+ async def identity(self) -> None:
83
+ """Nobodies is identified."""
84
+ return None
85
+
86
+ async def authenticated_userid(self) -> str | UUID:
87
+ """An uuid mades of 0."""
88
+ return UUID(int=0)
89
+
90
+ async def has_permission(
91
+ self, permission: str
92
+ ) -> HasPermission | type[HasPermission]:
93
+ """Access is allways granted."""
94
+ return Allowed
95
+
96
+ async def remember(self, user: None) -> None:
97
+ """Do nothing."""
98
+
99
+ async def forget(self) -> None:
100
+ """Do nothing."""
@@ -13,9 +13,7 @@ from collections.abc import Mapping
13
13
  from typing import Any
14
14
 
15
15
  from fastlife import Request, Response
16
- from fastlife.templates.inline import InlineTemplate
17
-
18
- TemplateParams = Mapping[str, Any]
16
+ from fastlife.domain.model.template import InlineTemplate
19
17
 
20
18
 
21
19
  class AbstractTemplateRenderer(abc.ABC):
@@ -77,9 +75,6 @@ class AbstractTemplateRenderer(abc.ABC):
77
75
  class AbstractTemplateRendererFactory(abc.ABC):
78
76
  """
79
77
  The template render factory.
80
-
81
- The implementation of this class is found using the settings
82
- :attr:`fastlife.config.settings.Settings.template_renderer_class`.
83
78
  """
84
79
 
85
80
  @abc.abstractmethod
@@ -64,6 +64,9 @@ class Localizer:
64
64
  self.translations[domain].merge(trans)
65
65
  self.global_translations.merge(trans)
66
66
 
67
+ def __call__(self, message: str, mapping: dict[str, str] | None = None) -> str:
68
+ return self.gettext(message, mapping)
69
+
67
70
  def gettext(self, message: str, mapping: dict[str, str] | None = None) -> str:
68
71
  ret = self.global_translations.gettext(message)
69
72
  if mapping:
@@ -29,15 +29,9 @@ class Settings(BaseSettings):
29
29
  a python module name. for instance `fastlife:components` is the directory components
30
30
  found in the fastlife package.
31
31
  """
32
- registry_class: str = Field(default="fastlife.config.registry:DefaultRegistry")
32
+ registry_class: str = Field(default="fastlife.service.registry:DefaultRegistry")
33
33
  """Implementation class for the application regitry."""
34
- template_renderer_class: str = Field(
35
- default="fastlife.templates.renderer:JinjaxEngine"
36
- )
37
- """
38
- Implementation class for the
39
- {class}`fastlife.templates.renderer.AbstractTemplateRenderer`.
40
- """
34
+
41
35
  form_data_model_prefix: str = Field(default="payload")
42
36
  """
43
37
  Pydantic form default model prefix for serialized field in www-urlencoded-form.
@@ -70,12 +64,12 @@ class Settings(BaseSettings):
70
64
  Set to true while developing, set false for production.
71
65
  """
72
66
  jinjax_global_catalog_class: str = Field(
73
- default="fastlife.templates.constants:Constants"
67
+ default="fastlife.template_globals:Globals"
74
68
  )
75
69
  """
76
70
  Set global constants accessible in every templates.
77
- Defaults to `fastlife.templates.constants:Constants`
78
- See {class}`fastlife.templates.constants.Constants`
71
+ Defaults to `fastlife.template_globals:Globals`
72
+ See {class}`fastlife.template_globals.Globals`
79
73
  """
80
74
 
81
75
  session_secret_key: str = Field(default="")
@@ -101,7 +95,7 @@ class Settings(BaseSettings):
101
95
  should be true while using https on production.
102
96
  """
103
97
  session_serializer: str = Field(
104
- default="fastlife.middlewares.session.serializer:SignedSessionSerializer"
98
+ default="fastlife.adapters.itsdangerous:SignedSessionSerializer"
105
99
  )
106
100
  """Cookie serializer for the session cookie."""
107
101
 
@@ -1,10 +1,14 @@
1
1
  """Type inference."""
2
2
 
3
+ import inspect
4
+ from collections.abc import Callable
3
5
  from types import UnionType
4
- from typing import Any, Union, get_origin
6
+ from typing import Any, Union, get_args, get_origin
5
7
 
6
8
  from pydantic import BaseModel
7
9
 
10
+ from fastlife.domain.model.template import InlineTemplate
11
+
8
12
 
9
13
  def is_complex_type(typ: type[Any]) -> bool:
10
14
  """
@@ -25,3 +29,22 @@ def is_union(typ: type[Any]) -> bool:
25
29
  if type_origin is UnionType: # T | U
26
30
  return True
27
31
  return False
32
+
33
+
34
+ def is_inline_template_returned(endpoint: Callable[..., Any]) -> bool:
35
+ """Test if a view, the endpoint return a template."""
36
+ signature = inspect.signature(endpoint)
37
+ return_annotation = signature.return_annotation
38
+
39
+ if isinstance(return_annotation, type) and issubclass(
40
+ return_annotation, InlineTemplate
41
+ ):
42
+ return True
43
+
44
+ if is_union(return_annotation):
45
+ return any(
46
+ isinstance(arg, type) and issubclass(arg, InlineTemplate)
47
+ for arg in get_args(return_annotation)
48
+ )
49
+
50
+ return False
@@ -16,7 +16,7 @@ def space_join(*segments: str) -> str:
16
16
  return " ".join(segments)
17
17
 
18
18
 
19
- class Constants(BaseModel):
19
+ class Globals(BaseModel):
20
20
  """Templates constants."""
21
21
 
22
22
  A_CLASS: str = space_join(
@@ -315,7 +315,7 @@ class Constants(BaseModel):
315
315
  SUMMARY_CLASS: str = "flex items-center items-center font-medium cursor-pointer"
316
316
  """Default css class for {jinjax:component}`Summary`."""
317
317
 
318
- TABLE_CLASS: str = "table-auto w-full text-left border-colapse"
318
+ TABLE_CLASS: str = "table-auto w-full text-left border-collapse"
319
319
  """Default css class for {jinjax:component}`Table`."""
320
320
 
321
321
  TD_CLASS: str = "px-4 py-2 font-normal border-b dark:border-neutral-500"
@@ -8,10 +8,10 @@ import bs4
8
8
  import httpx
9
9
  from fastapi.testclient import TestClient
10
10
  from multidict import MultiDict
11
- from starlette.types import ASGIApp
12
11
 
13
- from fastlife.config.settings import Settings
12
+ from fastlife.domain.model.asgi import ASGIApp
14
13
  from fastlife.middlewares.session.serializer import AbsractSessionSerializer
14
+ from fastlife.settings import Settings
15
15
  from fastlife.shared_utils.resolver import resolve
16
16
  from fastlife.testing.dom import Element
17
17
  from fastlife.testing.form import WebForm
@@ -0,0 +1 @@
1
+ """Fastlife views."""
@@ -1,3 +1,9 @@
1
+ """
2
+ Views for pydantic form.
3
+
4
+ Pydantic form generate form that may contains fields that requires some ajax query.
5
+ """
6
+
1
7
  from typing import cast
2
8
 
3
9
  from fastapi import Query
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: High-level web framework
5
5
  Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
6
6
  License: MIT License