fastlifeweb 0.22.1__py3-none-any.whl → 0.23.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 CHANGED
@@ -1,3 +1,14 @@
1
+ ## 0.23.0 - Released on 2024-12-04
2
+ * Update Request type.
3
+ * Breaking changes: Request[TUser, TRegistry] -> Request[TRegistry, TIdentity, TClaimedIdentity].
4
+ * Update SecurityPolicy, designed for MFA by default.
5
+ * Breaking changes: new abstract method added. build_authentication_state.
6
+ * Breaking changes: there is no more get_authenticated_userid method.
7
+ * The identity method is not abstract anymore, result comes from the build_authentication_state.
8
+ * New method get_authentication_state, claimed_identity and pre_remember.
9
+ * Add a AbstractNoMFASecurityPolicy that build a AbstractSecurityPolicy without TClaimedIdentity as None.
10
+ * New ACL type added to raise 401 errors due to missing MFA which may not be same url as tu login/password.
11
+
1
12
  ## 0.22.1 - Released on 2024-11-27
2
13
  * Improve Request typing
3
14
 
fastlife/__init__.py CHANGED
@@ -27,9 +27,15 @@ from .domain.model.form import FormModel
27
27
  from .domain.model.request import GenericRequest
28
28
  from .domain.model.security_policy import (
29
29
  Allowed,
30
+ Anonymous,
31
+ Authenticated,
32
+ AuthenticationState,
30
33
  Denied,
31
34
  Forbidden,
32
35
  HasPermission,
36
+ NoMFAAuthenticationState,
37
+ PendingMFA,
38
+ PreAuthenticated,
33
39
  Unauthenticated,
34
40
  Unauthorized,
35
41
  )
@@ -37,7 +43,11 @@ from .domain.model.template import JinjaXTemplate
37
43
 
38
44
  # from .request.form_data import model
39
45
  from .service.registry import DefaultRegistry, GenericRegistry, TRegistry, TSettings
40
- from .service.security_policy import AbstractSecurityPolicy, InsecurePolicy
46
+ from .service.security_policy import (
47
+ AbstractNoMFASecurityPolicy,
48
+ AbstractSecurityPolicy,
49
+ InsecurePolicy,
50
+ )
41
51
  from .settings import Settings
42
52
 
43
53
  __all__ = [
@@ -69,13 +79,20 @@ __all__ = [
69
79
  "RedirectResponse",
70
80
  # Security
71
81
  "AbstractSecurityPolicy",
82
+ "AbstractNoMFASecurityPolicy",
72
83
  "HasPermission",
73
84
  "Unauthenticated",
85
+ "PreAuthenticated",
74
86
  "Allowed",
75
87
  "Denied",
76
88
  "Unauthorized",
77
89
  "Forbidden",
78
90
  "InsecurePolicy",
91
+ "Anonymous",
92
+ "PendingMFA",
93
+ "Authenticated",
94
+ "AuthenticationState",
95
+ "NoMFAAuthenticationState",
79
96
  # Template
80
97
  "JinjaXTemplate",
81
98
  # i18n
@@ -9,18 +9,18 @@ from fastlife.domain.model.request import GenericRequest
9
9
  from fastlife.service.registry import DefaultRegistry
10
10
 
11
11
 
12
- def get_request(request: FastAPIRequest) -> GenericRequest[Any, Any]:
12
+ def get_request(request: FastAPIRequest) -> GenericRequest[Any, Any, Any]:
13
13
  """Return the Fastlife Request object."""
14
14
  return request # type: ignore
15
15
 
16
16
 
17
- Request = Annotated[GenericRequest[Any, DefaultRegistry], Depends(get_request)]
17
+ Request = Annotated[GenericRequest[DefaultRegistry, Any, Any], Depends(get_request)]
18
18
  """A request that is associated to the default registry."""
19
19
  # FastAPI handle its Request objects using a lenient_issubclass,
20
20
  # basically a issubclass(Request), does not work with Generic[T].
21
21
 
22
22
 
23
- AnyRequest = Annotated[GenericRequest[Any, Any], Depends(get_request)]
23
+ AnyRequest = Annotated[GenericRequest[Any, Any, Any], Depends(get_request)]
24
24
  """A request version that is associated to the any registry."""
25
25
 
26
26
 
@@ -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 = GenericRequest[Any, Any](self._registry, request)
44
+ req = GenericRequest[Any, Any, Any](self._registry, request)
45
45
  return await orig_route_handler(req)
46
46
 
47
47
  return route_handler
@@ -144,7 +144,7 @@ class GenericConfigurator(Generic[TRegistry]):
144
144
  self._route_prefix: str = ""
145
145
  self._routers: dict[str, Router] = defaultdict(Router)
146
146
  self._security_policies: dict[
147
- str, type[AbstractSecurityPolicy[Any, TRegistry]]
147
+ str, type[AbstractSecurityPolicy[Any, Any, TRegistry]]
148
148
  ] = {}
149
149
 
150
150
  self._registered_permissions: set[str] = set()
@@ -322,7 +322,7 @@ class GenericConfigurator(Generic[TRegistry]):
322
322
  return self
323
323
 
324
324
  def set_security_policy(
325
- self, security_policy: "type[AbstractSecurityPolicy[Any, TRegistry]]"
325
+ self, security_policy: "type[AbstractSecurityPolicy[TRegistry, Any, Any]]"
326
326
  ) -> Self:
327
327
  """
328
328
  Set a security policy for the application.
@@ -594,7 +594,7 @@ class GenericConfigurator(Generic[TRegistry]):
594
594
  # class is wrong.
595
595
  # Until we store a security policy per rooter, we rebuild an
596
596
  # incomplete request here.
597
- req = GenericRequest[Any, DefaultRegistry](self.registry, request)
597
+ req = GenericRequest[DefaultRegistry, Any, Any](self.registry, request)
598
598
  resp = handler(req, exc)
599
599
  if isinstance(resp, Response):
600
600
  return resp
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Generic
5
5
  from starlette.requests import Request as BaseRequest
6
6
 
7
7
  from fastlife.domain.model.csrf import CSRFToken, create_csrf_token
8
- from fastlife.domain.model.security_policy import TUser
8
+ from fastlife.domain.model.security_policy import TClaimedIdentity, TIdentity
9
9
  from fastlife.service.registry import TRegistry
10
10
 
11
11
  if TYPE_CHECKING:
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  )
16
16
 
17
17
 
18
- class GenericRequest(BaseRequest, Generic[TUser, TRegistry]):
18
+ class GenericRequest(BaseRequest, Generic[TRegistry, TIdentity, TClaimedIdentity]):
19
19
  """HTTP Request representation."""
20
20
 
21
21
  registry: TRegistry
@@ -23,7 +23,9 @@ class GenericRequest(BaseRequest, Generic[TUser, TRegistry]):
23
23
  locale_name: str
24
24
  """Request locale used for the i18n of the response."""
25
25
 
26
- security_policy: "AbstractSecurityPolicy[TUser, TRegistry] | None"
26
+ security_policy: (
27
+ "AbstractSecurityPolicy[TRegistry, TIdentity, TClaimedIdentity] | None"
28
+ )
27
29
  """Request locale used for the i18n of the response."""
28
30
 
29
31
  renderer_globals: dict[str, Any]
@@ -2,7 +2,7 @@
2
2
 
3
3
  import logging
4
4
  from collections.abc import Callable, Coroutine
5
- from typing import Any, Literal, TypeVar
5
+ from typing import Any, Generic, Literal, TypeVar
6
6
 
7
7
  from fastapi import HTTPException
8
8
  from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
@@ -10,13 +10,62 @@ from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
10
10
  CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
11
11
  CheckPermission = Callable[[str], CheckPermissionHook]
12
12
 
13
- TUser = TypeVar("TUser")
13
+ TClaimedIdentity = TypeVar("TClaimedIdentity")
14
+ TIdentity = TypeVar("TIdentity")
14
15
 
15
16
  log = logging.getLogger(__name__)
16
17
 
17
18
 
19
+ class _Anonymous: ...
20
+
21
+
22
+ Anonymous = _Anonymous()
23
+ """
24
+ The user is not authenticated.
25
+ """
26
+
27
+
28
+ class PendingMFA(Generic[TClaimedIdentity]):
29
+ """
30
+ The user provided its identity, usually validated with a first factor,
31
+ such as a password but it has not totally proved its authentication
32
+ by a second or many other factors of authentication.
33
+ The type TClaimedIdentity will store the relevant informations during
34
+ this authentication phase.
35
+ """
36
+
37
+ claimed: TClaimedIdentity | None
38
+ __match_args__ = ("claimed",)
39
+
40
+ def __init__(self, claimed: TClaimedIdentity) -> None:
41
+ self.claimed = claimed
42
+
43
+
44
+ class Authenticated(Generic[TIdentity]):
45
+ """The identity has been validated."""
46
+
47
+ __match_args__ = ("identity",)
48
+
49
+ def __init__(self, identity: TIdentity) -> None:
50
+ self.identity = identity
51
+
52
+
53
+ AuthenticationState = (
54
+ _Anonymous | PendingMFA[TClaimedIdentity] | Authenticated[TIdentity]
55
+ )
56
+ """
57
+ Type representing the state of an authentication.
58
+ """
59
+
60
+ NoMFAAuthenticationState = AuthenticationState[None, TIdentity]
61
+ """
62
+ Type representing a state of authentication when no multiple factor of authentication
63
+ is involved.
64
+ """
65
+
66
+
18
67
  class Unauthorized(HTTPException):
19
- """An exception raised to stop a request exectution and return an HTTP Error."""
68
+ """An exception raised to stop a request execution and return a 401 HTTP Error."""
20
69
 
21
70
  def __init__(
22
71
  self,
@@ -27,8 +76,22 @@ class Unauthorized(HTTPException):
27
76
  super().__init__(status_code, detail, headers)
28
77
 
29
78
 
79
+ class MFARequired(Unauthorized):
80
+ """
81
+ An exception raised to stop a request execution and return a 401 HTTP Error for MFA.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ status_code: int = HTTP_401_UNAUTHORIZED,
87
+ detail: str = "MFA Required",
88
+ headers: dict[str, str] | None = None,
89
+ ) -> None:
90
+ super().__init__(status_code, detail, headers)
91
+
92
+
30
93
  class Forbidden(HTTPException):
31
- """An exception raised to stop a request exectution and return an HTTP Error."""
94
+ """An exception raised to stop a request execution and return a 403 HTTP Error."""
32
95
 
33
96
  def __init__(
34
97
  self,
@@ -59,7 +122,7 @@ class HasPermission(int, metaclass=BoolMeta):
59
122
  or the frontend may use the information to adapt its interface.
60
123
  """
61
124
 
62
- kind: Literal["allowed", "unauthenticated", "denied"]
125
+ kind: Literal["allowed", "unauthenticated", "mfa_required", "denied"]
63
126
  """
64
127
  Identified basic information of the response.
65
128
  It distinguished unauthenticated and denied to eventually raised 401 over 403 error.
@@ -96,6 +159,16 @@ class Unauthenticated(HasPermission):
96
159
  reason = "Authentication required"
97
160
 
98
161
 
162
+ class PreAuthenticated(HasPermission):
163
+ """
164
+ Represent a permission check result that is not allowed due to
165
+ missing secondary authentication mechanism.
166
+ """
167
+
168
+ kind = "mfa_required"
169
+ reason = "MFA required"
170
+
171
+
99
172
  class Denied(HasPermission):
100
173
  """
101
174
  Represent a permission check result that is not allowed due to lack of permission.
@@ -34,6 +34,8 @@ def check_permission(permission_name: str) -> CheckPermissionHook:
34
34
  return
35
35
  case "denied":
36
36
  raise request.security_policy.Forbidden(detail=allowed.reason)
37
+ case "mfa_required":
38
+ raise request.security_policy.MFARequired(detail=allowed.reason)
37
39
  case "unauthenticated":
38
40
  raise request.security_policy.Unauthorized(detail=allowed.reason)
39
41
 
@@ -10,14 +10,16 @@ LocaleName = str
10
10
 
11
11
  from fastlife.adapters.fastapi.request import GenericRequest # coverage: ignore
12
12
 
13
- LocaleNegociator = Callable[[GenericRequest[Any, Any]], LocaleName] # coverage: ignore
13
+ LocaleNegociator = Callable[
14
+ [GenericRequest[Any, Any, Any]], LocaleName
15
+ ] # coverage: ignore
14
16
  """Interface to implement to negociate a locale""" # coverage: ignore
15
17
 
16
18
 
17
19
  def default_negociator(settings: Settings) -> LocaleNegociator:
18
20
  """The default local negociator return the locale set in the conf."""
19
21
 
20
- def locale_negociator(request: "GenericRequest[Any, Any]") -> str:
22
+ def locale_negociator(request: "GenericRequest[Any, Any, Any]") -> str:
21
23
  return settings.default_locale
22
24
 
23
25
  return locale_negociator
@@ -2,34 +2,44 @@
2
2
 
3
3
  import abc
4
4
  from typing import Annotated, Any, Generic
5
- from uuid import UUID
6
5
 
7
6
  from fastapi import Depends
8
7
 
9
8
  from fastlife import GenericRequest, get_request
10
9
  from fastlife.domain.model.security_policy import (
11
10
  Allowed,
11
+ Anonymous,
12
+ Authenticated,
13
+ AuthenticationState,
12
14
  Forbidden,
13
15
  HasPermission,
14
- TUser,
16
+ MFARequired,
17
+ PendingMFA,
18
+ TClaimedIdentity,
19
+ TIdentity,
15
20
  Unauthorized,
16
21
  )
17
22
  from fastlife.service.registry import TRegistry
18
23
 
19
24
 
20
- class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]):
25
+ class AbstractSecurityPolicy(abc.ABC, Generic[TRegistry, TIdentity, TClaimedIdentity]):
21
26
  """Security policy base class."""
22
27
 
23
28
  Forbidden = Forbidden
24
29
  """The exception raised if the user identified is not granted."""
25
30
  Unauthorized = Unauthorized
26
31
  """The exception raised if no user has been identified."""
32
+ MFARequired = MFARequired
33
+ """The exception raised if no user has been authenticated using a MFA."""
27
34
 
28
- request: GenericRequest[TUser, TRegistry]
35
+ request: GenericRequest[TRegistry, TIdentity, TClaimedIdentity]
29
36
  """Request where the security policy is applied."""
30
37
 
31
38
  def __init__(
32
- self, request: Annotated[GenericRequest[TUser, TRegistry], Depends(get_request)]
39
+ self,
40
+ request: Annotated[
41
+ GenericRequest[TRegistry, TIdentity, TClaimedIdentity], Depends(get_request)
42
+ ],
33
43
  ):
34
44
  """
35
45
  Build the security policy.
@@ -42,17 +52,48 @@ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]):
42
52
  """
43
53
  self.request = request
44
54
  self.request.security_policy = self # we do backref to implement has_permission
55
+ self._authentication_state: (
56
+ AuthenticationState[TClaimedIdentity, TIdentity] | None
57
+ ) = None
45
58
 
46
- @abc.abstractmethod
47
- async def identity(self) -> TUser | None:
59
+ async def get_authentication_state(
60
+ self,
61
+ ) -> AuthenticationState[TClaimedIdentity, TIdentity]:
62
+ """
63
+ Return app-specific user object or None.
64
+ """
65
+ if self._authentication_state is None:
66
+ self._authentication_state = await self.build_authentication_state()
67
+ return self._authentication_state
68
+
69
+ async def claimed_identity(self) -> TClaimedIdentity | None:
70
+ """
71
+ Return app-specific user object that pretend to be identified.
72
+ """
73
+ auth = await self.get_authentication_state()
74
+ match auth:
75
+ case PendingMFA(claimed):
76
+ return claimed
77
+ case _:
78
+ return None
79
+
80
+ async def identity(self) -> TIdentity | None:
48
81
  """
49
- Return app-specific user object or raise an HTTPException.
82
+ Return app-specific user object after an mfa authentication or None.
50
83
  """
84
+ auth = await self.get_authentication_state()
85
+ match auth:
86
+ case Authenticated(identity):
87
+ return identity
88
+ case _:
89
+ return None
51
90
 
52
91
  @abc.abstractmethod
53
- async def authenticated_userid(self) -> str | UUID | None:
92
+ async def build_authentication_state(
93
+ self,
94
+ ) -> AuthenticationState[TClaimedIdentity, TIdentity]:
54
95
  """
55
- Return app-specific user object or raise an HTTPException.
96
+ Return the authentication state for the current request.
56
97
  """
57
98
 
58
99
  @abc.abstractmethod
@@ -62,7 +103,11 @@ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]):
62
103
  """Allow access to everything if signed in."""
63
104
 
64
105
  @abc.abstractmethod
65
- async def remember(self, user: TUser) -> None:
106
+ async def pre_remember(self, claimed_identity: TClaimedIdentity) -> None:
107
+ """Save the user identity in the request session."""
108
+
109
+ @abc.abstractmethod
110
+ async def remember(self, identity: TIdentity) -> None:
66
111
  """Save the user identity in the request session."""
67
112
 
68
113
  @abc.abstractmethod
@@ -70,7 +115,12 @@ class AbstractSecurityPolicy(abc.ABC, Generic[TUser, TRegistry]):
70
115
  """Destroy the request session."""
71
116
 
72
117
 
73
- class InsecurePolicy(AbstractSecurityPolicy[None, Any]):
118
+ class AbstractNoMFASecurityPolicy(AbstractSecurityPolicy[TRegistry, TIdentity, None]):
119
+ async def pre_remember(self, claimed_identity: None) -> None:
120
+ """Do Nothing."""
121
+
122
+
123
+ class InsecurePolicy(AbstractNoMFASecurityPolicy[Any, None]):
74
124
  """
75
125
  An implementation of the security policy made for explicit unsecured access.
76
126
 
@@ -79,13 +129,10 @@ class InsecurePolicy(AbstractSecurityPolicy[None, Any]):
79
129
  or your own reason, the InsecurePolicy has to be set to the configurator.
80
130
  """
81
131
 
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)
132
+ async def build_authentication_state(
133
+ self,
134
+ ) -> AuthenticationState[None, None]:
135
+ return Anonymous
89
136
 
90
137
  async def has_permission(
91
138
  self, permission: str
@@ -93,7 +140,7 @@ class InsecurePolicy(AbstractSecurityPolicy[None, Any]):
93
140
  """Access is allways granted."""
94
141
  return Allowed
95
142
 
96
- async def remember(self, user: None) -> None:
143
+ async def remember(self, identity: None) -> None:
97
144
  """Do nothing."""
98
145
 
99
146
  async def forget(self) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.22.1
3
+ Version: 0.23.0
4
4
  Summary: High-level web framework
5
5
  Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
6
6
  License: MIT License
@@ -1,13 +1,13 @@
1
- CHANGELOG.md,sha256=iMVndHirSKsb0IAqVoiHUmMU4uYt7fwML0j96qwPYUc,7049
2
- fastlife/__init__.py,sha256=cPPHF7zBVBkUt6KsJyVNiamuRRjUeERQsPVoS6AB-YE,1779
1
+ CHANGELOG.md,sha256=Ap14kfVx07rFhITGDmtQfwEsJowg5osxKETE0w478lU,7801
2
+ fastlife/__init__.py,sha256=cx3BScbBelH-Tm63VPdAp5siW1fBZ0uOMwNW_SPs2xQ,2126
3
3
  fastlife/adapters/__init__.py,sha256=imPD1hImpgrYkvUJRhHA5kVyGAua7VbP2WGkhSWKJT8,93
4
4
  fastlife/adapters/fastapi/__init__.py,sha256=1goV1FGFP04TGyskJBLKZam4Gvt1yoAvLMNs4ekWSSQ,243
5
5
  fastlife/adapters/fastapi/form.py,sha256=csxsDI6RK-g41pMwFhaVQCLDhF7dAZzgUp-VcrC3NFY,823
6
6
  fastlife/adapters/fastapi/form_data.py,sha256=2DQ0o-RvY6iROUKQjS-UJdNYEVSsNPd-AjpergI3w54,4473
7
7
  fastlife/adapters/fastapi/localizer.py,sha256=XD1kCJuAlkGevivmvAJEcGMCBWMef9rAfTOGmt3PVWU,436
8
- fastlife/adapters/fastapi/request.py,sha256=hvJS7qxH7ZyKRdwDalVXGU8ZH84NtSQuljnAZPyqogU,1100
8
+ fastlife/adapters/fastapi/request.py,sha256=COOoSMZAm4VhyJgM7dlqJ7YdGjeGI7qs93PtBsriEPc,1115
9
9
  fastlife/adapters/fastapi/routing/__init__.py,sha256=8EMnQE5n8oA4J9_c3nxzwKDVt3tefZ6fGH0d2owE8mo,195
10
- fastlife/adapters/fastapi/routing/route.py,sha256=Y4LTTTgQAR5zPMV3hKBP-YYjEQC41UUIa5ZuPCPf2aY,1471
10
+ fastlife/adapters/fastapi/routing/route.py,sha256=XnDPvd5V0Zl7Ke6bBErEtUCjmNQPcV2U_w1dWpx6qM4,1476
11
11
  fastlife/adapters/fastapi/routing/router.py,sha256=jzrnU_Lyywu21e3spPaWQw8ujZh_Yy_EJOojcCi6ew4,499
12
12
  fastlife/adapters/itsdangerous/__init__.py,sha256=7ocGY7v0cxooZBKQYjA2JkmzRqiBvcU1uzA84UsTVAI,84
13
13
  fastlife/adapters/itsdangerous/session.py,sha256=9h_WRsXqZbytHZOv5B_K3OWD5mbfYzxHulXoOf6D2MI,1685
@@ -1686,7 +1686,7 @@ fastlife/components/pydantic_form/FatalError.jinja,sha256=lFVlNrXzBR6ExMahq77h0t
1686
1686
  fastlife/components/pydantic_form/Hint.jinja,sha256=8leBpfMGDmalc_KAjr2paTojr_rwq-luS6m_1BGj7Tw,202
1687
1687
  fastlife/components/pydantic_form/Widget.jinja,sha256=PgguUpvhG6CY9AW6H8qQMjKqjlybjDCAaFFAOHzrzVQ,418
1688
1688
  fastlife/config/__init__.py,sha256=5qpuaVYqi-AS0GgsfggM6rFsSwXgrqrLBo9jH6dVroc,407
1689
- fastlife/config/configurator.py,sha256=AC4s4iM_DPDkKDs7DP6GJFwMt-HFqFtAHUcBnr1BGkY,24707
1689
+ fastlife/config/configurator.py,sha256=SURXmBrdTghHoG2f9R2BUF6TKZXg1lNmwP3ZbuApJ3M,24722
1690
1690
  fastlife/config/exceptions.py,sha256=9MdBnbfy-Aw-KaIFzju0Kh8Snk41-v9LqK2w48Tdy1s,1169
1691
1691
  fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
1692
1692
  fastlife/config/resources.py,sha256=u6OgnbHfGkC5idH-YPNkIPf8GJnZpJoGVZ-Ym022BCo,8533
@@ -1696,8 +1696,8 @@ fastlife/domain/model/__init__.py,sha256=aoBjaSpDscuFXvtknJHwiNyoJRUpE-v4X54h_wN
1696
1696
  fastlife/domain/model/asgi.py,sha256=RSTnfTsofOmCaWzHNuRGowjlyHYmoDCrXFbvNY_B55k,129
1697
1697
  fastlife/domain/model/csrf.py,sha256=BUiWK-S7rVciWHO1qTkM8e_KxzpF6gGC4MMJK1v6iDo,414
1698
1698
  fastlife/domain/model/form.py,sha256=WriBT1qUUIbf5x5iewo9ChEcr6k0en8jMTD0iaei5Pk,3253
1699
- fastlife/domain/model/request.py,sha256=hMbp1OWY1BIWmqFe-kEvwOOXBB6t1ylVaHR30UbQ0_Y,2596
1700
- fastlife/domain/model/security_policy.py,sha256=iYBKXOYaXxM_n-rsyB25lO6RblSx9icTx1Bg-s3Iz9k,2942
1699
+ fastlife/domain/model/request.py,sha256=ZRHZW_MOmtO_DFHt2UYu_aUmtoMdD14085A8Z8_eS8s,2678
1700
+ fastlife/domain/model/security_policy.py,sha256=f9SLi54vvRU-KSPJ5K0unoqYpkxIyzuZjKf2Ylwf5Rg,4796
1701
1701
  fastlife/domain/model/template.py,sha256=z9oxdKme1hMPuvk7mBiKR_tuVY8TqH77aTYqMgvEGl8,876
1702
1702
  fastlife/domain/model/types.py,sha256=64jJKFAi5x0e3vr8naHU1m_as0Qy8MS-s9CG0z6K1qc,381
1703
1703
  fastlife/middlewares/__init__.py,sha256=C3DUOzR5EhlAv5Zq7h-Abyvkd7bUsJohTRSB2wpRYQE,220
@@ -1709,11 +1709,11 @@ fastlife/middlewares/session/middleware.py,sha256=ituZ5hNipDMkgCXNE4zbnmOcWEF151
1709
1709
  fastlife/middlewares/session/serializer.py,sha256=nbJGiCJ_ryZxkW1I28kmK6hD3U98D4ZlUQA7B8_tngQ,635
1710
1710
  fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1711
1711
  fastlife/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1712
- fastlife/service/check_permission.py,sha256=-XD9qA11dLwen0bNCTy2Id_dCjzTJ0j7xuwB9IOtEY0,1695
1712
+ fastlife/service/check_permission.py,sha256=-TsI58YZJtWIw5bsm0fVpfuaCMUx4cmoLTKGXeyQPDk,1809
1713
1713
  fastlife/service/csrf.py,sha256=wC1PaKOmZ3il0FF_kevxnlg9PxDqruRdLrNnOA3ZHrU,1886
1714
- fastlife/service/locale_negociator.py,sha256=Dq0k7lvUWOmd0o2xJFneymOu-H9z-RpSYU_AZ2IYF-g,780
1714
+ fastlife/service/locale_negociator.py,sha256=JUqzTukxDqTJVOR-CNI7Vqo6kvdvwxYvZQe8P3V9S2U,796
1715
1715
  fastlife/service/registry.py,sha256=B6n5b_b0RgxJj0qFOpnrJFmG7_MPtvShwV6yH9V6vi0,2098
1716
- fastlife/service/security_policy.py,sha256=fOyhaLNI3EzW5p_s3ZcrDaf7cX-KEWb7fQANoQSk6yg,3050
1716
+ fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
1717
1717
  fastlife/service/templates.py,sha256=QPAIUbbZiekazz_jV3q4JCwQd6Q4KA6a4RDek2RWuhE,2548
1718
1718
  fastlife/service/translations.py,sha256=D-1D3pVNytEcps1u-0K7FmgQ8Wo6Yu4XVHvZrPhBmAI,5795
1719
1719
  fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
@@ -1728,9 +1728,9 @@ fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2
1728
1728
  fastlife/testing/testclient.py,sha256=JTIgeMKooA8L4gEodeC3gy4Lo27y3WNswSEIKLlVVPs,6745
1729
1729
  fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
1730
1730
  fastlife/views/pydantic_form.py,sha256=o7EUItciAGL1OSaGNHo-3BTrYAk34GuWE7zGikjiAGY,1486
1731
- fastlifeweb-0.22.1.dist-info/METADATA,sha256=UO41tIM5bFp2QZF-GO1aXFa-k6FN85JShOEsFUsCuZA,3663
1732
- fastlifeweb-0.22.1.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
1733
- fastlifeweb-0.22.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1734
- fastlifeweb-0.22.1.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
1731
+ fastlifeweb-0.23.0.dist-info/METADATA,sha256=Eww8hBxH7oR5_EeqtALpdmUmJr2f1vs1E0EijXaxM30,3663
1732
+ fastlifeweb-0.23.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
1733
+ fastlifeweb-0.23.0.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1734
+ fastlifeweb-0.23.0.dist-info/licenses/LICENSE,sha256=NlRX9Z-dcv8X1VFW9odlIQBbgNN9pcO94XzvKp2R16o,1075
1735
1735
  tailwind.config.js,sha256=EN3EahBDmQBbmJvkw3SdGWNOkfkzw0cg-QvBikOhkrw,1348
1736
- fastlifeweb-0.22.1.dist-info/RECORD,,
1736
+ fastlifeweb-0.23.0.dist-info/RECORD,,