fastlifeweb 0.25.2__py3-none-any.whl → 0.26.1__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,10 @@
1
+ ## 0.26.1 - Released on 2025-04-20
2
+ * Add new helpers for the webtestclient element.
3
+
4
+ ## 0.26.0 - Released on 2025-03-08
5
+ * Add a new method to the configuration to customize the request class.
6
+ See [Configurator.set_request_factory](#fastlife.config.configurator.GenericConfigurator.set_request_factory)
7
+
1
8
  ## 0.25.2 - Released on 2025-03-03
2
9
  * Update deps and fix related bugs in JinjaX components.
3
10
 
fastlife/__init__.py CHANGED
@@ -23,6 +23,7 @@ from .config import (
23
23
  resource_view,
24
24
  view_config,
25
25
  )
26
+ from .domain.model.asgi import ASGIRequest, ASGIResponse
26
27
  from .domain.model.form import FormModel
27
28
  from .domain.model.request import GenericRequest
28
29
  from .domain.model.security_policy import (
@@ -36,6 +37,8 @@ from .domain.model.security_policy import (
36
37
  NoMFAAuthenticationState,
37
38
  PendingMFA,
38
39
  PreAuthenticated,
40
+ TClaimedIdentity,
41
+ TIdentity,
39
42
  Unauthenticated,
40
43
  Unauthorized,
41
44
  )
@@ -43,6 +46,7 @@ from .domain.model.template import JinjaXTemplate
43
46
 
44
47
  # from .request.form_data import model
45
48
  from .service.registry import DefaultRegistry, GenericRegistry, TRegistry, TSettings
49
+ from .service.request_factory import RequestFactory
46
50
  from .service.security_policy import (
47
51
  AbstractNoMFASecurityPolicy,
48
52
  AbstractSecurityPolicy,
@@ -75,6 +79,10 @@ __all__ = [
75
79
  "AnyRequest",
76
80
  "Request",
77
81
  "get_request",
82
+ # Request Factory
83
+ "ASGIRequest",
84
+ "ASGIResponse",
85
+ "RequestFactory",
78
86
  # Response
79
87
  "Response",
80
88
  "RedirectResponse",
@@ -94,6 +102,8 @@ __all__ = [
94
102
  "Authenticated",
95
103
  "AuthenticationState",
96
104
  "NoMFAAuthenticationState",
105
+ "TClaimedIdentity",
106
+ "TIdentity",
97
107
  # Template
98
108
  "JinjaXTemplate",
99
109
  # i18n
@@ -4,10 +4,8 @@ from collections.abc import Callable, Coroutine
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from fastapi.routing import APIRoute
7
- from starlette.requests import Request as StarletteRequest
8
- from starlette.responses import Response
9
7
 
10
- from fastlife.domain.model.request import GenericRequest
8
+ from fastlife.domain.model.asgi import ASGIRequest, ASGIResponse
11
9
 
12
10
  if TYPE_CHECKING:
13
11
  from fastlife.service.registry import DefaultRegistry # coverage: ignore
@@ -34,14 +32,14 @@ class Route(APIRoute):
34
32
 
35
33
  def get_route_handler(
36
34
  self,
37
- ) -> Callable[[StarletteRequest], Coroutine[Any, Any, Response]]:
35
+ ) -> Callable[[ASGIRequest], Coroutine[Any, Any, ASGIResponse]]:
38
36
  """
39
37
  Replace the request object by the fastlife request associated with the registry.
40
38
  """
41
39
  orig_route_handler = super().get_route_handler()
42
40
 
43
- async def route_handler(request: StarletteRequest) -> Response:
44
- req = GenericRequest[Any, Any, Any](self._registry, request)
41
+ async def route_handler(request: ASGIRequest) -> ASGIResponse:
42
+ req = self._registry.request_factory(request)
45
43
  return await orig_route_handler(req)
46
44
 
47
45
  return route_handler
fastlife/assets/dist.css CHANGED
@@ -1,4 +1,5 @@
1
- /*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
1
+ /*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
2
+ @layer properties;
2
3
  @layer theme, base, components, utilities;
3
4
  @layer theme {
4
5
  :root, :host {
@@ -6,20 +7,20 @@
6
7
  'Noto Color Emoji';
7
8
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
8
9
  monospace;
9
- --color-red-50: oklch(0.971 0.013 17.38);
10
- --color-red-400: oklch(0.704 0.191 22.216);
11
- --color-red-700: oklch(0.505 0.213 27.518);
12
- --color-orange-500: oklch(0.705 0.213 47.604);
13
- --color-neutral-50: oklch(0.985 0 0);
14
- --color-neutral-100: oklch(0.97 0 0);
15
- --color-neutral-200: oklch(0.922 0 0);
16
- --color-neutral-300: oklch(0.87 0 0);
17
- --color-neutral-400: oklch(0.708 0 0);
18
- --color-neutral-500: oklch(0.556 0 0);
19
- --color-neutral-600: oklch(0.439 0 0);
20
- --color-neutral-700: oklch(0.371 0 0);
21
- --color-neutral-800: oklch(0.269 0 0);
22
- --color-neutral-900: oklch(0.205 0 0);
10
+ --color-red-50: oklch(97.1% 0.013 17.38);
11
+ --color-red-400: oklch(70.4% 0.191 22.216);
12
+ --color-red-700: oklch(50.5% 0.213 27.518);
13
+ --color-orange-500: oklch(70.5% 0.213 47.604);
14
+ --color-neutral-50: oklch(98.5% 0 0);
15
+ --color-neutral-100: oklch(97% 0 0);
16
+ --color-neutral-200: oklch(92.2% 0 0);
17
+ --color-neutral-300: oklch(87% 0 0);
18
+ --color-neutral-400: oklch(70.8% 0 0);
19
+ --color-neutral-500: oklch(55.6% 0 0);
20
+ --color-neutral-600: oklch(43.9% 0 0);
21
+ --color-neutral-700: oklch(37.1% 0 0);
22
+ --color-neutral-800: oklch(26.9% 0 0);
23
+ --color-neutral-900: oklch(20.5% 0 0);
23
24
  --color-white: #fff;
24
25
  --spacing: 0.25rem;
25
26
  --text-sm: 0.875rem;
@@ -48,11 +49,7 @@
48
49
  --default-transition-duration: 150ms;
49
50
  --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
50
51
  --default-font-family: var(--font-sans);
51
- --default-font-feature-settings: var(--font-sans--font-feature-settings);
52
- --default-font-variation-settings: var(--font-sans--font-variation-settings);
53
52
  --default-mono-font-family: var(--font-mono);
54
- --default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
55
- --default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
56
53
  }
57
54
  }
58
55
  @layer base {
@@ -66,14 +63,11 @@
66
63
  line-height: 1.5;
67
64
  -webkit-text-size-adjust: 100%;
68
65
  tab-size: 4;
69
- font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' );
66
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
70
67
  font-feature-settings: var(--default-font-feature-settings, normal);
71
68
  font-variation-settings: var(--default-font-variation-settings, normal);
72
69
  -webkit-tap-highlight-color: transparent;
73
70
  }
74
- body {
75
- line-height: inherit;
76
- }
77
71
  hr {
78
72
  height: 0;
79
73
  color: inherit;
@@ -96,7 +90,7 @@
96
90
  font-weight: bolder;
97
91
  }
98
92
  code, kbd, samp, pre {
99
- font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace );
93
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
100
94
  font-feature-settings: var(--default-mono-font-feature-settings, normal);
101
95
  font-variation-settings: var(--default-mono-font-variation-settings, normal);
102
96
  font-size: 1em;
@@ -162,7 +156,14 @@
162
156
  }
163
157
  ::placeholder {
164
158
  opacity: 1;
165
- color: color-mix(in oklab, currentColor 50%, transparent);
159
+ }
160
+ @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
161
+ ::placeholder {
162
+ color: currentcolor;
163
+ @supports (color: color-mix(in lab, red, red)) {
164
+ color: color-mix(in oklab, currentcolor 50%, transparent);
165
+ }
166
+ }
166
167
  }
167
168
  textarea {
168
169
  resize: vertical;
@@ -215,24 +216,6 @@
215
216
  .static {
216
217
  position: static;
217
218
  }
218
- .container {
219
- width: 100%;
220
- @media (width >= 40rem) {
221
- max-width: 40rem;
222
- }
223
- @media (width >= 48rem) {
224
- max-width: 48rem;
225
- }
226
- @media (width >= 64rem) {
227
- max-width: 64rem;
228
- }
229
- @media (width >= 80rem) {
230
- max-width: 80rem;
231
- }
232
- @media (width >= 96rem) {
233
- max-width: 96rem;
234
- }
235
- }
236
219
  .m-3 {
237
220
  margin: calc(var(--spacing) * 3);
238
221
  }
@@ -263,9 +246,6 @@
263
246
  .inline {
264
247
  display: inline;
265
248
  }
266
- .table {
267
- display: table;
268
- }
269
249
  .h-4 {
270
250
  height: calc(var(--spacing) * 4);
271
251
  }
@@ -303,7 +283,7 @@
303
283
  rotate: 90deg;
304
284
  }
305
285
  .transform {
306
- transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
286
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
307
287
  }
308
288
  .cursor-pointer {
309
289
  cursor: pointer;
@@ -494,18 +474,19 @@
494
474
  }
495
475
  .focus\:ring-2 {
496
476
  &:focus {
497
- --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
477
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
498
478
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
499
479
  }
500
480
  }
501
481
  .focus\:ring-4 {
502
482
  &:focus {
503
- --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
483
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
504
484
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
505
485
  }
506
486
  }
507
487
  .focus\:outline-hidden {
508
488
  &:focus {
489
+ --tw-outline-style: none;
509
490
  outline-style: none;
510
491
  @media (forced-colors: active) {
511
492
  outline: 2px solid transparent;
@@ -618,27 +599,22 @@
618
599
  @property --tw-rotate-x {
619
600
  syntax: "*";
620
601
  inherits: false;
621
- initial-value: rotateX(0);
622
602
  }
623
603
  @property --tw-rotate-y {
624
604
  syntax: "*";
625
605
  inherits: false;
626
- initial-value: rotateY(0);
627
606
  }
628
607
  @property --tw-rotate-z {
629
608
  syntax: "*";
630
609
  inherits: false;
631
- initial-value: rotateZ(0);
632
610
  }
633
611
  @property --tw-skew-x {
634
612
  syntax: "*";
635
613
  inherits: false;
636
- initial-value: skewX(0);
637
614
  }
638
615
  @property --tw-skew-y {
639
616
  syntax: "*";
640
617
  inherits: false;
641
- initial-value: skewY(0);
642
618
  }
643
619
  @property --tw-space-y-reverse {
644
620
  syntax: "*";
@@ -707,6 +683,19 @@
707
683
  syntax: "*";
708
684
  inherits: false;
709
685
  }
686
+ @property --tw-drop-shadow-color {
687
+ syntax: "*";
688
+ inherits: false;
689
+ }
690
+ @property --tw-drop-shadow-alpha {
691
+ syntax: "<percentage>";
692
+ inherits: false;
693
+ initial-value: 100%;
694
+ }
695
+ @property --tw-drop-shadow-size {
696
+ syntax: "*";
697
+ inherits: false;
698
+ }
710
699
  @property --tw-duration {
711
700
  syntax: "*";
712
701
  inherits: false;
@@ -720,6 +709,11 @@
720
709
  syntax: "*";
721
710
  inherits: false;
722
711
  }
712
+ @property --tw-shadow-alpha {
713
+ syntax: "<percentage>";
714
+ inherits: false;
715
+ initial-value: 100%;
716
+ }
723
717
  @property --tw-inset-shadow {
724
718
  syntax: "*";
725
719
  inherits: false;
@@ -729,6 +723,11 @@
729
723
  syntax: "*";
730
724
  inherits: false;
731
725
  }
726
+ @property --tw-inset-shadow-alpha {
727
+ syntax: "<percentage>";
728
+ inherits: false;
729
+ initial-value: 100%;
730
+ }
732
731
  @property --tw-ring-color {
733
732
  syntax: "*";
734
733
  inherits: false;
@@ -766,3 +765,48 @@
766
765
  inherits: false;
767
766
  initial-value: 0 0 #0000;
768
767
  }
768
+ @layer properties {
769
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
770
+ *, ::before, ::after, ::backdrop {
771
+ --tw-rotate-x: initial;
772
+ --tw-rotate-y: initial;
773
+ --tw-rotate-z: initial;
774
+ --tw-skew-x: initial;
775
+ --tw-skew-y: initial;
776
+ --tw-space-y-reverse: 0;
777
+ --tw-border-style: solid;
778
+ --tw-leading: initial;
779
+ --tw-font-weight: initial;
780
+ --tw-tracking: initial;
781
+ --tw-outline-style: solid;
782
+ --tw-blur: initial;
783
+ --tw-brightness: initial;
784
+ --tw-contrast: initial;
785
+ --tw-grayscale: initial;
786
+ --tw-hue-rotate: initial;
787
+ --tw-invert: initial;
788
+ --tw-opacity: initial;
789
+ --tw-saturate: initial;
790
+ --tw-sepia: initial;
791
+ --tw-drop-shadow: initial;
792
+ --tw-drop-shadow-color: initial;
793
+ --tw-drop-shadow-alpha: 100%;
794
+ --tw-drop-shadow-size: initial;
795
+ --tw-duration: initial;
796
+ --tw-shadow: 0 0 #0000;
797
+ --tw-shadow-color: initial;
798
+ --tw-shadow-alpha: 100%;
799
+ --tw-inset-shadow: 0 0 #0000;
800
+ --tw-inset-shadow-color: initial;
801
+ --tw-inset-shadow-alpha: 100%;
802
+ --tw-ring-color: initial;
803
+ --tw-ring-shadow: 0 0 #0000;
804
+ --tw-inset-ring-color: initial;
805
+ --tw-inset-ring-shadow: 0 0 #0000;
806
+ --tw-ring-inset: initial;
807
+ --tw-ring-offset-width: 0px;
808
+ --tw-ring-offset-color: #fff;
809
+ --tw-ring-offset-shadow: 0 0 #0000;
810
+ }
811
+ }
812
+ }
@@ -44,13 +44,15 @@ from fastlife.shared_utils.resolver import (
44
44
  )
45
45
 
46
46
  if TYPE_CHECKING:
47
+ from fastlife.service.locale_negociator import LocaleNegociator # coverage: ignore
48
+ from fastlife.service.request_factory import (
49
+ RequestFactoryBuilder, # coverage: ignore
50
+ )
47
51
  from fastlife.service.security_policy import AbstractSecurityPolicy
48
52
  from fastlife.service.templates import (
49
53
  AbstractTemplateRendererFactory, # coverage: ignore
50
54
  )
51
55
 
52
- from fastlife.service.locale_negociator import LocaleNegociator
53
-
54
56
  log = logging.getLogger(__name__)
55
57
  VENUSIAN_CATEGORY = "fastlife"
56
58
 
@@ -175,7 +177,7 @@ class GenericConfigurator(Generic[TRegistry]):
175
177
  """
176
178
 
177
179
  # register our main template renderer at then end, to ensure that
178
- # if settings have been manipulated, everythins is taken into account.
180
+ # if settings have been manipulated, everything is taken into account.
179
181
  self.add_renderer(
180
182
  self.registry.settings.jinjax_file_ext,
181
183
  resolve("fastlife.adapters.jinjax.renderer:JinjaxEngine")(
@@ -260,7 +262,14 @@ class GenericConfigurator(Generic[TRegistry]):
260
262
  self._route_prefix = old
261
263
  return self
262
264
 
263
- def set_locale_negociator(self, locale_negociator: LocaleNegociator) -> Self:
265
+ def set_request_factory(
266
+ self, request_factory: "RequestFactoryBuilder[TRegistry]"
267
+ ) -> Self:
268
+ """Install a request factory, to use a custom request classes."""
269
+ self.registry.request_factory = request_factory(self.registry)
270
+ return self
271
+
272
+ def set_locale_negociator(self, locale_negociator: "LocaleNegociator") -> Self:
264
273
  """Install a locale negociator for the app."""
265
274
  self.registry.locale_negociator = locale_negociator
266
275
  return self
@@ -1,3 +1,21 @@
1
+ """ASGI types from Starlette."""
2
+
3
+ from starlette.requests import Request
4
+ from starlette.responses import Response
1
5
  from starlette.types import ASGIApp, Message, Receive, Scope, Send
2
6
 
3
- __all__ = ["ASGIApp", "Message", "Receive", "Scope", "Send"]
7
+ ASGIRequest = Request
8
+ """Starlette request class used as ASGI Protocol base HTTP Request representation."""
9
+
10
+ ASGIResponse = Response
11
+ """Starlette request class used as ASGI Protocol base HTTP Response representation."""
12
+
13
+ __all__ = [
14
+ "ASGIApp",
15
+ "ASGIRequest",
16
+ "ASGIResponse",
17
+ "Message",
18
+ "Receive",
19
+ "Scope",
20
+ "Send",
21
+ ]
@@ -2,8 +2,7 @@
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Generic
4
4
 
5
- from starlette.requests import Request as BaseRequest
6
-
5
+ from fastlife.domain.model.asgi import ASGIRequest
7
6
  from fastlife.domain.model.csrf import CSRFToken, create_csrf_token
8
7
  from fastlife.domain.model.security_policy import TClaimedIdentity, TIdentity
9
8
  from fastlife.service.registry import TRegistry
@@ -15,7 +14,7 @@ if TYPE_CHECKING:
15
14
  )
16
15
 
17
16
 
18
- class GenericRequest(BaseRequest, Generic[TRegistry, TIdentity, TClaimedIdentity]):
17
+ class GenericRequest(ASGIRequest, Generic[TRegistry, TIdentity, TClaimedIdentity]):
19
18
  """HTTP Request representation."""
20
19
 
21
20
  registry: TRegistry
@@ -30,7 +29,7 @@ class GenericRequest(BaseRequest, Generic[TRegistry, TIdentity, TClaimedIdentity
30
29
 
31
30
  renderer_globals: dict[str, Any]
32
31
 
33
- def __init__(self, registry: TRegistry, request: BaseRequest) -> None:
32
+ def __init__(self, registry: TRegistry, request: ASGIRequest) -> None:
34
33
  super().__init__(request.scope, request.receive)
35
34
  self.registry = registry
36
35
  self.locale_name = registry.locale_negociator(self)
@@ -0,0 +1 @@
1
+ """A collection of service."""
@@ -1,4 +1,4 @@
1
- """Security policy."""
1
+ """Security policy permission routine."""
2
2
 
3
3
  from collections.abc import Callable, Coroutine
4
4
  from typing import Any
@@ -8,12 +8,10 @@ from fastlife.settings import Settings
8
8
  LocaleName = str
9
9
  """The LocaleName is a locale such as en, fr that will be consume for translations."""
10
10
 
11
- from fastlife.adapters.fastapi.request import GenericRequest # coverage: ignore
11
+ from fastlife.adapters.fastapi.request import GenericRequest
12
12
 
13
- LocaleNegociator = Callable[
14
- [GenericRequest[Any, Any, Any]], LocaleName
15
- ] # coverage: ignore
16
- """Interface to implement to negociate a locale""" # coverage: ignore
13
+ LocaleNegociator = Callable[[GenericRequest[Any, Any, Any]], LocaleName]
14
+ """Interface to implement to negociate a locale"""
17
15
 
18
16
 
19
17
  def default_negociator(settings: Settings) -> LocaleNegociator:
@@ -1,3 +1,5 @@
1
+ """Application registry."""
2
+
1
3
  from collections.abc import AsyncIterator, Mapping
2
4
  from contextlib import asynccontextmanager
3
5
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
@@ -6,6 +8,7 @@ from fastapi import FastAPI
6
8
 
7
9
  if TYPE_CHECKING:
8
10
  from fastlife.service.locale_negociator import LocaleNegociator # coverage: ignore
11
+ from fastlife.service.request_factory import RequestFactory # coverage: ignore
9
12
  from fastlife.service.templates import ( # coverage: ignore
10
13
  AbstractTemplateRendererFactory, # coverage: ignore
11
14
  ) # coverage: ignore
@@ -33,15 +36,18 @@ class GenericRegistry(Generic[TSettings]):
33
36
  """Used to fine the best language for the response."""
34
37
  localizer: "LocalizerFactory"
35
38
  """Used to localized message."""
39
+ request_factory: "RequestFactory"
36
40
 
37
41
  def __init__(self, settings: TSettings) -> None:
38
42
  from fastlife.service.locale_negociator import default_negociator
43
+ from fastlife.service.request_factory import default_request_factory
39
44
  from fastlife.service.translations import LocalizerFactory
40
45
 
41
46
  self.settings = settings
42
47
  self.locale_negociator = default_negociator(self.settings)
43
48
  self.renderers = {}
44
49
  self.localizer = LocalizerFactory()
50
+ self.request_factory = default_request_factory(self)
45
51
 
46
52
  def get_renderer(self, template: str) -> "AbstractTemplateRendererFactory":
47
53
  for key, val in self.renderers.items():
@@ -0,0 +1,26 @@
1
+ """Customize the request class."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from fastlife.domain.model.asgi import ASGIRequest
7
+ from fastlife.domain.model.request import GenericRequest
8
+ from fastlife.service.registry import DefaultRegistry, TRegistry
9
+
10
+ RequestFactory = Callable[[ASGIRequest], GenericRequest[Any, Any, Any]]
11
+ """
12
+ Transform the [ASGIRequest](#fastlife.domain.model.asgi.ASGIRequest)
13
+ object to the fastlife [GenericRequest](#fastlife.domain.model.request.GenericRequest).
14
+ """
15
+
16
+ RequestFactoryBuilder = Callable[[TRegistry], RequestFactory]
17
+ """Interface to implement to create a request factory"""
18
+
19
+
20
+ def default_request_factory(registry: DefaultRegistry) -> RequestFactory:
21
+ """The default request factory the return the generic request."""
22
+
23
+ def request(request: ASGIRequest) -> GenericRequest[Any, Any, Any]:
24
+ return GenericRequest[Any, Any, Any](registry, request)
25
+
26
+ return request
@@ -1,3 +1,5 @@
1
+ """Implement i18n."""
2
+
1
3
  import pathlib
2
4
  from collections import defaultdict
3
5
  from collections.abc import Callable, Iterator
@@ -15,7 +17,7 @@ class TranslatableString(str):
15
17
  """
16
18
  Create a string made for translation associated to a domain.
17
19
  This class is instanciated by the
18
- :class:`fastlife.service.translations.TranslatableStringFactory` class.
20
+ {class}`fastlife.service.translations.TranslatableStringFactory` class.
19
21
  """
20
22
 
21
23
  __slots__ = ("domain",)
fastlife/testing/dom.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import re
4
4
  from collections.abc import Iterator, Sequence
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Literal, overload
6
6
 
7
7
  import bs4
8
8
 
@@ -82,10 +82,19 @@ class Element:
82
82
  el = el.parent
83
83
  return None
84
84
 
85
- def by_text(self, text: str, *, node_name: str | None = None) -> "Element | None":
85
+ def by_text(
86
+ self, text: str, *, node_name: str | None = None, position: int | None = None
87
+ ) -> "Element | None":
86
88
  """Find the first element that match the text."""
87
89
  nodes = self.iter_all_by_text(text, node_name=node_name)
88
- return next(nodes, None)
90
+ ret = list(nodes)
91
+ if not ret:
92
+ return None
93
+ if position is None:
94
+ assert len(ret) == 1, f"Should have 1 element, got {len(ret)} in {self}"
95
+ else:
96
+ assert len(ret) > position, "Not enough element found"
97
+ return ret[position or 0]
89
98
 
90
99
  def iter_all_by_text(
91
100
  self, text: str, *, node_name: str | None = None
@@ -121,17 +130,60 @@ class Element:
121
130
  assert not isinstance(resp, bs4.NavigableString)
122
131
  return Element(self._client, resp) if resp else None
123
132
 
133
+ def by_id(self, id: str) -> "Element | None":
134
+ """Find the element having the given id."""
135
+ resp = self._tag.find_all(id=id)
136
+ assert not isinstance(resp, bs4.NavigableString)
137
+ if not resp:
138
+ return None
139
+ assert len(resp) == 1
140
+ return Element(self._client, resp[0]) if resp else None
141
+
142
+ @overload
143
+ def by_node_name(
144
+ self,
145
+ node_name: str,
146
+ *,
147
+ attrs: dict[str, str] | None = None,
148
+ multiple: Literal[False],
149
+ ) -> "Element": ...
150
+
151
+ @overload
152
+ def by_node_name(
153
+ self,
154
+ node_name: str,
155
+ *,
156
+ attrs: dict[str, str] | None = None,
157
+ multiple: Literal[True],
158
+ ) -> "list[Element]": ...
159
+
160
+ @overload
161
+ def by_node_name(
162
+ self,
163
+ node_name: str,
164
+ *,
165
+ attrs: dict[str, str] | None = None,
166
+ ) -> "list[Element]": ...
167
+
124
168
  def by_node_name(
125
- self, node_name: str, *, attrs: dict[str, str] | None = None
126
- ) -> list["Element"]:
169
+ self,
170
+ node_name: str,
171
+ *,
172
+ attrs: dict[str, str] | None = None,
173
+ multiple: bool = True,
174
+ ) -> "list[Element] | Element":
127
175
  """
128
176
  Return the list of elements with the given node_name.
129
177
 
130
178
  An optional set of attributes may given and must match if passed.
131
179
  """
132
- return [
180
+ ret = [
133
181
  Element(self._client, e) for e in self._tag.find_all(node_name, attrs or {})
134
182
  ]
183
+ if not multiple:
184
+ assert len(ret) == 1
185
+ return ret[0]
186
+ return ret
135
187
 
136
188
  def __repr__(self) -> str:
137
189
  return f"<{self.node_name}>"
@@ -79,9 +79,11 @@ class WebResponse:
79
79
  self._form = WebForm(self._client, self._origin, form)
80
80
  return self._form
81
81
 
82
- def by_text(self, text: str, *, node_name: str | None = None) -> Element | None:
82
+ def by_text(
83
+ self, text: str, *, node_name: str | None = None, position: int | None = None
84
+ ) -> Element | None:
83
85
  """Search a dom element by its text."""
84
- return self.html.by_text(text, node_name=node_name)
86
+ return self.html.by_text(text, node_name=node_name, position=position)
85
87
 
86
88
  def by_label_text(self, text: str) -> Element | None:
87
89
  """Search a dom element by its associated label text."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastlifeweb
3
- Version: 0.25.2
3
+ Version: 0.26.1
4
4
  Summary: High-level web framework
5
5
  Author-Email: Guillaume Gauvrit <guillaume@gauvr.it>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
- CHANGELOG.md,sha256=lrPhY2hvBQErCpC_v376kDsXv_K2LTj4DgK7XWC_dsk,8373
2
- fastlife/__init__.py,sha256=nXWE4AbhkhG_yBjPJU-XnKDMTsU9ebv7Vj4eIciWQI0,2219
1
+ CHANGELOG.md,sha256=T96sQ3Cf4MXmutaDj0-js6kwr0hVzDJA9krCjrV557g,8680
2
+ fastlife/__init__.py,sha256=IZ9VQvlBduZ3WjfDk05rZNdCKNxWWqPaMvG0ZWtMTpU,2489
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
@@ -7,7 +7,7 @@ fastlife/adapters/fastapi/form_data.py,sha256=2DQ0o-RvY6iROUKQjS-UJdNYEVSsNPd-Aj
7
7
  fastlife/adapters/fastapi/localizer.py,sha256=Efn6rrf-SnSfM4TqqE_5chacrxaPpupxbvIqXipXEEw,448
8
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=XnDPvd5V0Zl7Ke6bBErEtUCjmNQPcV2U_w1dWpx6qM4,1476
10
+ fastlife/adapters/fastapi/routing/route.py,sha256=33nk0mf9eTOrdyQfeoOGOs5153TzT227sem0THWvr8k,1367
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
@@ -42,7 +42,7 @@ fastlife/adapters/jinjax/widgets/model.py,sha256=YBIEWa_6mnmrBnesXjLTrpJ4drUS2CI
42
42
  fastlife/adapters/jinjax/widgets/sequence.py,sha256=dVoHQmHloaRuU1Sd82b2jnO8WDfdwM2FaZlLCJCps1o,2566
43
43
  fastlife/adapters/jinjax/widgets/text.py,sha256=TfmlJU233aZWIl-4cmm-f-pFxp6ycHWHnbiluOvRDgM,3040
44
44
  fastlife/adapters/jinjax/widgets/union.py,sha256=roCoFA82dLjF1XFW6UYaV7SCQWdFsSAT8Ux7KEB6_Us,2602
45
- fastlife/assets/dist.css,sha256=BkGYK48Fmy7t3-KMKlpGnpjmpJpxQ4D6QUNlfmtVbU8,18432
45
+ fastlife/assets/dist.css,sha256=d2ez-igscOaeYtJQc2FQuXlN17cYX13sUansFdg_kdA,19753
46
46
  fastlife/assets/source.css,sha256=0KtDcsKHj9LOcqNR1iv9pACwNBaNWkieEDqqjkgNL_s,47
47
47
  fastlife/components/A.jinja,sha256=MDNJ2auIeYbpNeErvJdlGid4nIKfbi85ArmMgChsCJU,1384
48
48
  fastlife/components/Button.jinja,sha256=itKU-ct45XissU33yfmTekyHsNe00fr4RQL-e9cxbgU,2305
@@ -1690,17 +1690,17 @@ fastlife/components/pydantic_form/FatalError.jinja,sha256=ADtQvmo-e-NmDcFM1E6wZV
1690
1690
  fastlife/components/pydantic_form/Hint.jinja,sha256=8leBpfMGDmalc_KAjr2paTojr_rwq-luS6m_1BGj7Tw,202
1691
1691
  fastlife/components/pydantic_form/Widget.jinja,sha256=PgguUpvhG6CY9AW6H8qQMjKqjlybjDCAaFFAOHzrzVQ,418
1692
1692
  fastlife/config/__init__.py,sha256=5qpuaVYqi-AS0GgsfggM6rFsSwXgrqrLBo9jH6dVroc,407
1693
- fastlife/config/configurator.py,sha256=wlgSfbKoeBzk5kmCnftmrgVV8CWQhxoioBZeuRTPZ64,24779
1693
+ fastlife/config/configurator.py,sha256=2LkPXoarMTk40S7AVlxODPAckDllE0G8u27jtPmvGM8,25188
1694
1694
  fastlife/config/exceptions.py,sha256=9MdBnbfy-Aw-KaIFzju0Kh8Snk41-v9LqK2w48Tdy1s,1169
1695
1695
  fastlife/config/openapiextra.py,sha256=rYoerrn9sni2XwnO3gIWqaz7M0aDZPhVLjzqhDxue0o,514
1696
1696
  fastlife/config/resources.py,sha256=EcPTM25pnHcGFTtXjeZnWn5Mo_-8rhJ72HJ6rxnjPg8,8389
1697
1697
  fastlife/config/views.py,sha256=9CZ0qNi8vKvQuGo1GgM6cwNK8WwHOxwIHqtikAOaOHY,2399
1698
1698
  fastlife/domain/__init__.py,sha256=3zDDos5InVX0el9OO0lgSDGzdUNYIhlA6w4uhBh2pF8,29
1699
1699
  fastlife/domain/model/__init__.py,sha256=aoBjaSpDscuFXvtknJHwiNyoJRUpE-v4X54h_wNuo2Y,27
1700
- fastlife/domain/model/asgi.py,sha256=RSTnfTsofOmCaWzHNuRGowjlyHYmoDCrXFbvNY_B55k,129
1700
+ fastlife/domain/model/asgi.py,sha256=Cz45TZOtrh2pBVZr37aJ9jpnJH9BeNHrsvk9bq1nBc0,526
1701
1701
  fastlife/domain/model/csrf.py,sha256=BUiWK-S7rVciWHO1qTkM8e_KxzpF6gGC4MMJK1v6iDo,414
1702
1702
  fastlife/domain/model/form.py,sha256=JP6uumlZBYhiPxzcdxOsfsFm5BRfvkDFvlUCD6Vy8dI,3275
1703
- fastlife/domain/model/request.py,sha256=ZRHZW_MOmtO_DFHt2UYu_aUmtoMdD14085A8Z8_eS8s,2678
1703
+ fastlife/domain/model/request.py,sha256=HgUSnUu3q18e07y57PadN3pPQwYrIZS1YEhYkBZ_Zfg,2674
1704
1704
  fastlife/domain/model/security_policy.py,sha256=f9SLi54vvRU-KSPJ5K0unoqYpkxIyzuZjKf2Ylwf5Rg,4796
1705
1705
  fastlife/domain/model/template.py,sha256=z9oxdKme1hMPuvk7mBiKR_tuVY8TqH77aTYqMgvEGl8,876
1706
1706
  fastlife/domain/model/types.py,sha256=64jJKFAi5x0e3vr8naHU1m_as0Qy8MS-s9CG0z6K1qc,381
@@ -1712,29 +1712,29 @@ fastlife/middlewares/session/__init__.py,sha256=ZhXWXs53A__F9wJKBJ87rW8Qyt5Mn866
1712
1712
  fastlife/middlewares/session/middleware.py,sha256=tzaJHLT3ri9sstrppATu8MWXUALTq54PsNKU0v5DTBI,3133
1713
1713
  fastlife/middlewares/session/serializer.py,sha256=nbJGiCJ_ryZxkW1I28kmK6hD3U98D4ZlUQA7B8_tngQ,635
1714
1714
  fastlife/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1715
- fastlife/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1716
- fastlife/service/check_permission.py,sha256=-TsI58YZJtWIw5bsm0fVpfuaCMUx4cmoLTKGXeyQPDk,1809
1715
+ fastlife/service/__init__.py,sha256=SfM2eSrMjDx6safjBc2LVFty4Wy2H1ZsHQSHeDcZ7dU,31
1716
+ fastlife/service/check_permission.py,sha256=WodiDVTnY-dZmTfbQV-YmJfPOLtE07mfqvG6jvL0xeA,1828
1717
1717
  fastlife/service/csrf.py,sha256=wC1PaKOmZ3il0FF_kevxnlg9PxDqruRdLrNnOA3ZHrU,1886
1718
- fastlife/service/locale_negociator.py,sha256=JUqzTukxDqTJVOR-CNI7Vqo6kvdvwxYvZQe8P3V9S2U,796
1719
- fastlife/service/registry.py,sha256=3lm7aUD7FdlV1lQUS73OuBlL55-O1DulPJdE0n6Epks,2648
1718
+ fastlife/service/locale_negociator.py,sha256=4HifgNkyI7DxR3_IdSUMG0UUY-JZeQsJ_MMSqiyFzgc,730
1719
+ fastlife/service/registry.py,sha256=0r8dVCF44JUugRctL9sDQjnHDV7SepH06OfkV6KE-4s,2937
1720
+ fastlife/service/request_factory.py,sha256=9o4B_78qrKPXQAq8A_RDhzAqCHdt6arV96Bq_JByyIM,931
1720
1721
  fastlife/service/security_policy.py,sha256=qYXs4mhfz_u4x59NhUkirqKYKQbFv9YrzyRuXj7mxE0,4688
1721
1722
  fastlife/service/templates.py,sha256=xNMKH-jNkEoCscO04H-QlzTqg-0pYbF_fc65xG-2rzs,2575
1722
- fastlife/service/translatablestring.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
1723
- fastlife/service/translations.py,sha256=s6qFZSXR-1vYxSr7RRH-mS-VjNaa8OTxR7-k6Ib7h0E,6878
1723
+ fastlife/service/translations.py,sha256=cAfvUlLM3KcgQjlD9PtEpZpTMctXKM_CUAmUeKw9n4M,6901
1724
1724
  fastlife/settings.py,sha256=q-rz4CEF2RQGow5-m-yZJOvdh3PPb2c1Q_ZLJGnu4VQ,3647
1725
1725
  fastlife/shared_utils/__init__.py,sha256=i66ytuf-Ezo7jSiNQHIsBMVIcB-tDX0tg28-pUOlhzE,26
1726
1726
  fastlife/shared_utils/infer.py,sha256=0GflLkaWJ-4LZ1Ig3moR-_o55wwJ_p_vJ4xo-yi3lyA,1406
1727
1727
  fastlife/shared_utils/resolver.py,sha256=Wb9cO2MWavpti63hju15xmwFMgaD5DsQaxikRpB39E8,3713
1728
1728
  fastlife/template_globals.py,sha256=bKcj6kSnQlzuOeoILA5oRxxzy6CsrBFMZv9S0w5XlTQ,9021
1729
1729
  fastlife/testing/__init__.py,sha256=VpxkS3Zp3t_hH8dBiLaGFGhsvt511dhBS_8fMoFXdmU,99
1730
- fastlife/testing/dom.py,sha256=dVzDoZokn-ii681UaEwAr-khM5KE-CHgXSSLSo24oH0,4489
1730
+ fastlife/testing/dom.py,sha256=q2GFrHWjwKMMTR0dsP3J-rXSxojZy8rOQ-07h2gfLKA,5869
1731
1731
  fastlife/testing/form.py,sha256=diiGfVMfNt19JTNUxlnbGfcbskR3ZMpk0Y-A57vfShc,7871
1732
1732
  fastlife/testing/session.py,sha256=LEFFbiR67_x_g-ioudkY0C7PycHdbDfaIaoo_G7GXQ8,2226
1733
- fastlife/testing/testclient.py,sha256=Id1tlA1ZapyW-8kUh2_U3lLteL64m3ERqOO7NAN7HEY,6922
1733
+ fastlife/testing/testclient.py,sha256=4LLw_QchEzdcdIobtIEzCABNebzyzVPEMj1tjdXQU_Y,6984
1734
1734
  fastlife/views/__init__.py,sha256=zG8gveL8e2zBdYx6_9jtZfpQ6qJT-MFnBY3xXkLwHZI,22
1735
1735
  fastlife/views/pydantic_form.py,sha256=o7EUItciAGL1OSaGNHo-3BTrYAk34GuWE7zGikjiAGY,1486
1736
- fastlifeweb-0.25.2.dist-info/METADATA,sha256=szZ9s9Rv4714GIS8XeM0PAWN9EbGfZ5qX7T6TcjaMhE,3690
1737
- fastlifeweb-0.25.2.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
1738
- fastlifeweb-0.25.2.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1739
- fastlifeweb-0.25.2.dist-info/licenses/LICENSE,sha256=JFWuiKYRXKKMEAsX0aZp3hBcju-HYflJ2rwJAGwbCJo,1080
1740
- fastlifeweb-0.25.2.dist-info/RECORD,,
1736
+ fastlifeweb-0.26.1.dist-info/METADATA,sha256=ZrtYyrVue0a2wbR9S0wALG-3COPtbvSZVKHitRfu3t4,3690
1737
+ fastlifeweb-0.26.1.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
1738
+ fastlifeweb-0.26.1.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1739
+ fastlifeweb-0.26.1.dist-info/licenses/LICENSE,sha256=JFWuiKYRXKKMEAsX0aZp3hBcju-HYflJ2rwJAGwbCJo,1080
1740
+ fastlifeweb-0.26.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.3)
2
+ Generator: pdm-backend (2.4.4)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1 +0,0 @@
1
-