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 +7 -0
- fastlife/__init__.py +10 -0
- fastlife/adapters/fastapi/routing/route.py +4 -6
- fastlife/assets/dist.css +98 -54
- fastlife/config/configurator.py +13 -4
- fastlife/domain/model/asgi.py +19 -1
- fastlife/domain/model/request.py +3 -4
- fastlife/service/__init__.py +1 -0
- fastlife/service/check_permission.py +1 -1
- fastlife/service/locale_negociator.py +3 -5
- fastlife/service/registry.py +6 -0
- fastlife/service/request_factory.py +26 -0
- fastlife/service/translations.py +3 -1
- fastlife/testing/dom.py +58 -6
- fastlife/testing/testclient.py +4 -2
- {fastlifeweb-0.25.2.dist-info → fastlifeweb-0.26.1.dist-info}/METADATA +1 -1
- {fastlifeweb-0.25.2.dist-info → fastlifeweb-0.26.1.dist-info}/RECORD +20 -20
- {fastlifeweb-0.25.2.dist-info → fastlifeweb-0.26.1.dist-info}/WHEEL +1 -1
- fastlife/service/translatablestring.py +0 -1
- {fastlifeweb-0.25.2.dist-info → fastlifeweb-0.26.1.dist-info}/entry_points.txt +0 -0
- {fastlifeweb-0.25.2.dist-info → fastlifeweb-0.26.1.dist-info}/licenses/LICENSE +0 -0
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.
|
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[[
|
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:
|
44
|
-
req =
|
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.
|
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(
|
10
|
-
--color-red-400: oklch(
|
11
|
-
--color-red-700: oklch(
|
12
|
-
--color-orange-500: oklch(
|
13
|
-
--color-neutral-50: oklch(
|
14
|
-
--color-neutral-100: oklch(
|
15
|
-
--color-neutral-200: oklch(
|
16
|
-
--color-neutral-300: oklch(
|
17
|
-
--color-neutral-400: oklch(
|
18
|
-
--color-neutral-500: oklch(
|
19
|
-
--color-neutral-600: oklch(
|
20
|
-
--color-neutral-700: oklch(
|
21
|
-
--color-neutral-800: oklch(
|
22
|
-
--color-neutral-900: oklch(
|
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(
|
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(
|
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
|
-
|
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,
|
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,
|
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
|
+
}
|
fastlife/config/configurator.py
CHANGED
@@ -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,
|
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
|
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
|
fastlife/domain/model/asgi.py
CHANGED
@@ -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
|
-
|
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
|
+
]
|
fastlife/domain/model/request.py
CHANGED
@@ -2,8 +2,7 @@
|
|
2
2
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Generic
|
4
4
|
|
5
|
-
from
|
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(
|
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:
|
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)
|
fastlife/service/__init__.py
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
"""A collection of service."""
|
@@ -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
|
11
|
+
from fastlife.adapters.fastapi.request import GenericRequest
|
12
12
|
|
13
|
-
LocaleNegociator = Callable[
|
14
|
-
|
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:
|
fastlife/service/registry.py
CHANGED
@@ -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
|
fastlife/service/translations.py
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
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,
|
126
|
-
|
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
|
-
|
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}>"
|
fastlife/testing/testclient.py
CHANGED
@@ -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(
|
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,5 +1,5 @@
|
|
1
|
-
CHANGELOG.md,sha256=
|
2
|
-
fastlife/__init__.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
1716
|
-
fastlife/service/check_permission.py,sha256
|
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=
|
1719
|
-
fastlife/service/registry.py,sha256=
|
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/
|
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=
|
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=
|
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.
|
1737
|
-
fastlifeweb-0.
|
1738
|
-
fastlifeweb-0.
|
1739
|
-
fastlifeweb-0.
|
1740
|
-
fastlifeweb-0.
|
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 +0,0 @@
|
|
1
|
-
|
File without changes
|
File without changes
|