fastlifeweb 0.16.3__py3-none-any.whl → 0.17.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.
- fastlife/adapters/jinjax/renderer.py +49 -25
- fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
- fastlife/adapters/jinjax/widget_factory/base.py +38 -0
- fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
- fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
- fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
- fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
- fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
- fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
- fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
- fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
- fastlife/adapters/jinjax/widgets/base.py +6 -4
- fastlife/adapters/jinjax/widgets/checklist.py +1 -1
- fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
- fastlife/adapters/jinjax/widgets/hidden.py +2 -0
- fastlife/adapters/jinjax/widgets/model.py +4 -1
- fastlife/adapters/jinjax/widgets/sequence.py +3 -2
- fastlife/adapters/jinjax/widgets/text.py +9 -10
- fastlife/adapters/jinjax/widgets/union.py +9 -7
- fastlife/components/Form.jinja +12 -0
- fastlife/config/configurator.py +23 -24
- fastlife/config/exceptions.py +4 -1
- fastlife/config/openapiextra.py +1 -0
- fastlife/config/resources.py +26 -27
- fastlife/config/settings.py +2 -0
- fastlife/config/views.py +3 -1
- fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
- fastlife/middlewares/session/middleware.py +2 -2
- fastlife/middlewares/session/serializer.py +6 -5
- fastlife/request/form.py +7 -6
- fastlife/request/form_data.py +2 -6
- fastlife/routing/route.py +3 -1
- fastlife/routing/router.py +1 -0
- fastlife/security/csrf.py +2 -1
- fastlife/security/policy.py +2 -1
- fastlife/services/locale_negociator.py +2 -1
- fastlife/services/policy.py +3 -2
- fastlife/services/templates.py +2 -1
- fastlife/services/translations.py +15 -8
- fastlife/shared_utils/infer.py +4 -3
- fastlife/shared_utils/resolver.py +64 -4
- fastlife/templates/binding.py +2 -1
- fastlife/testing/__init__.py +1 -0
- fastlife/testing/dom.py +140 -0
- fastlife/testing/form.py +204 -0
- fastlife/testing/session.py +67 -0
- fastlife/testing/testclient.py +7 -390
- fastlife/views/pydantic_form.py +4 -4
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
- fastlife/adapters/jinjax/widgets/factory.py +0 -525
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
"""Handle Sequence type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping, MutableSequence, Sequence
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic.fields import FieldInfo
|
7
|
+
|
8
|
+
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
9
|
+
from fastlife.adapters.jinjax.widgets.base import Widget
|
10
|
+
from fastlife.adapters.jinjax.widgets.sequence import SequenceWidget
|
11
|
+
|
12
|
+
|
13
|
+
class SequenceBuilder(BaseWidgetBuilder[Sequence[Any]]):
|
14
|
+
"""Builder for Sequence values."""
|
15
|
+
|
16
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
17
|
+
"""True for Sequence, MutableSequence or list"""
|
18
|
+
return origin is Sequence or origin is MutableSequence or origin is list
|
19
|
+
|
20
|
+
def build(
|
21
|
+
self,
|
22
|
+
*,
|
23
|
+
field_name: str,
|
24
|
+
field_type: type[Any],
|
25
|
+
field: FieldInfo | None,
|
26
|
+
value: Sequence[Any] | None,
|
27
|
+
form_errors: Mapping[str, Any],
|
28
|
+
removable: bool,
|
29
|
+
) -> Widget[Sequence[Any]]:
|
30
|
+
"""Build the widget."""
|
31
|
+
typ = field_type.__args__[0] # type: ignore
|
32
|
+
value = value or []
|
33
|
+
items: Sequence[Any] = [
|
34
|
+
self.factory.build(
|
35
|
+
typ, # type: ignore
|
36
|
+
name=f"{field_name}.{idx}",
|
37
|
+
value=v,
|
38
|
+
field=field,
|
39
|
+
form_errors=form_errors,
|
40
|
+
removable=True,
|
41
|
+
)
|
42
|
+
for idx, v in enumerate(value)
|
43
|
+
]
|
44
|
+
return SequenceWidget(
|
45
|
+
field_name,
|
46
|
+
title=field.title if field else "",
|
47
|
+
hint=field.description if field else None,
|
48
|
+
aria_label=(
|
49
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
50
|
+
if field and field.json_schema_extra
|
51
|
+
else None
|
52
|
+
),
|
53
|
+
value=items,
|
54
|
+
item_type=typ, # type: ignore
|
55
|
+
token=self.factory.token,
|
56
|
+
removable=removable,
|
57
|
+
error=form_errors.get(field_name),
|
58
|
+
)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"""Handle Set type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from enum import Enum
|
5
|
+
from typing import Any, Literal, get_origin
|
6
|
+
|
7
|
+
from pydantic.fields import FieldInfo
|
8
|
+
|
9
|
+
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
10
|
+
from fastlife.adapters.jinjax.widgets.base import Widget
|
11
|
+
from fastlife.adapters.jinjax.widgets.checklist import Checkable, ChecklistWidget
|
12
|
+
|
13
|
+
|
14
|
+
class SetBuilder(BaseWidgetBuilder[set[Any]]):
|
15
|
+
"""Builder for Set."""
|
16
|
+
|
17
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
18
|
+
"""True for Set"""
|
19
|
+
return origin is set
|
20
|
+
|
21
|
+
def build(
|
22
|
+
self,
|
23
|
+
*,
|
24
|
+
field_name: str,
|
25
|
+
field_type: type[Any],
|
26
|
+
field: FieldInfo | None,
|
27
|
+
value: set[Any] | None,
|
28
|
+
form_errors: Mapping[str, Any],
|
29
|
+
removable: bool,
|
30
|
+
) -> Widget[Any]:
|
31
|
+
"""Build the widget."""
|
32
|
+
choice_wrapper = field_type.__args__[0]
|
33
|
+
choices = []
|
34
|
+
choice_wrapper_origin = get_origin(choice_wrapper)
|
35
|
+
if choice_wrapper_origin:
|
36
|
+
if choice_wrapper_origin is Literal:
|
37
|
+
litchoice: list[str] = choice_wrapper.__args__ # type: ignore
|
38
|
+
choices = [
|
39
|
+
Checkable(
|
40
|
+
label=c,
|
41
|
+
value=c,
|
42
|
+
checked=c in value if value else False, # type: ignore
|
43
|
+
name=field_name,
|
44
|
+
token=self.factory.token,
|
45
|
+
error=form_errors.get(f"{field_name}-{c}"),
|
46
|
+
)
|
47
|
+
for c in litchoice
|
48
|
+
]
|
49
|
+
|
50
|
+
else:
|
51
|
+
raise NotImplementedError # coverage: ignore
|
52
|
+
elif issubclass(choice_wrapper, Enum):
|
53
|
+
choices = [
|
54
|
+
Checkable(
|
55
|
+
label=e.value,
|
56
|
+
value=e.name,
|
57
|
+
checked=e.name in value if value else False, # type: ignore
|
58
|
+
name=field_name,
|
59
|
+
token=self.factory.token,
|
60
|
+
error=form_errors.get(f"{field_name}-{e.name}"),
|
61
|
+
)
|
62
|
+
for e in choice_wrapper
|
63
|
+
]
|
64
|
+
else:
|
65
|
+
raise NotImplementedError # coverage: ignore
|
66
|
+
|
67
|
+
return ChecklistWidget(
|
68
|
+
field_name,
|
69
|
+
title=field.title if field else "",
|
70
|
+
hint=field.description if field else None,
|
71
|
+
aria_label=(
|
72
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
73
|
+
if field and field.json_schema_extra
|
74
|
+
else None
|
75
|
+
),
|
76
|
+
token=self.factory.token,
|
77
|
+
value=choices,
|
78
|
+
removable=removable,
|
79
|
+
error=form_errors.get(field_name),
|
80
|
+
)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
"""Handle simple types (str, int, float, ...)."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from decimal import Decimal
|
5
|
+
from typing import Any
|
6
|
+
from uuid import UUID
|
7
|
+
|
8
|
+
from pydantic.fields import FieldInfo
|
9
|
+
|
10
|
+
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
11
|
+
from fastlife.adapters.jinjax.widgets.base import Widget
|
12
|
+
from fastlife.adapters.jinjax.widgets.text import TextWidget
|
13
|
+
|
14
|
+
|
15
|
+
class SimpleTypeBuilder(BaseWidgetBuilder[str | int | str | float | Decimal | UUID]):
|
16
|
+
"""Builder for simple types."""
|
17
|
+
|
18
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
19
|
+
"""True for simple types: int, str, float, Decimal, UUID"""
|
20
|
+
return issubclass(typ, int | str | float | Decimal | UUID)
|
21
|
+
|
22
|
+
def build(
|
23
|
+
self,
|
24
|
+
*,
|
25
|
+
field_name: str,
|
26
|
+
field_type: type[Any],
|
27
|
+
field: FieldInfo | None,
|
28
|
+
value: int | str | float | Decimal | UUID | None,
|
29
|
+
form_errors: Mapping[str, Any],
|
30
|
+
removable: bool,
|
31
|
+
) -> Widget[int | str | float | Decimal | UUID]:
|
32
|
+
"""Build the widget."""
|
33
|
+
return TextWidget(
|
34
|
+
field_name,
|
35
|
+
placeholder=str(field.examples[0]) if field and field.examples else None,
|
36
|
+
title=field.title if field else "",
|
37
|
+
hint=field.description if field else None,
|
38
|
+
aria_label=(
|
39
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
40
|
+
if field and field.json_schema_extra
|
41
|
+
else None
|
42
|
+
),
|
43
|
+
removable=removable,
|
44
|
+
token=self.factory.token,
|
45
|
+
value=str(value) if value else "",
|
46
|
+
error=form_errors.get(field_name),
|
47
|
+
)
|
@@ -0,0 +1,90 @@
|
|
1
|
+
"""Handle Union type."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from types import NoneType
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from pydantic import ValidationError
|
8
|
+
from pydantic.fields import FieldInfo
|
9
|
+
|
10
|
+
from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
|
11
|
+
from fastlife.adapters.jinjax.widgets.base import Widget
|
12
|
+
from fastlife.adapters.jinjax.widgets.union import UnionWidget
|
13
|
+
from fastlife.shared_utils.infer import is_complex_type, is_union
|
14
|
+
|
15
|
+
|
16
|
+
class UnionBuilder(BaseWidgetBuilder[Any]):
|
17
|
+
"""Builder for Union."""
|
18
|
+
|
19
|
+
def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
|
20
|
+
"""True for unions Union[A,B], A | B or event Optional[A], A | None"""
|
21
|
+
return is_union(typ)
|
22
|
+
|
23
|
+
def build(
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
field_name: str,
|
27
|
+
field_type: type[Any],
|
28
|
+
field: FieldInfo | None,
|
29
|
+
value: Any | None,
|
30
|
+
form_errors: Mapping[str, Any],
|
31
|
+
removable: bool,
|
32
|
+
) -> Widget[Any]:
|
33
|
+
"""Build the widget."""
|
34
|
+
types: list[type[Any]] = []
|
35
|
+
# required = True
|
36
|
+
for typ in field_type.__args__: # type: ignore
|
37
|
+
if typ is NoneType:
|
38
|
+
# required = False
|
39
|
+
continue
|
40
|
+
types.append(typ) # type: ignore
|
41
|
+
|
42
|
+
if (
|
43
|
+
not removable
|
44
|
+
and len(types) == 1
|
45
|
+
# if the optional type is a complex type,
|
46
|
+
and not is_complex_type(types[0])
|
47
|
+
):
|
48
|
+
return self.factory.build( # coverage: ignore
|
49
|
+
types[0],
|
50
|
+
name=field_name,
|
51
|
+
field=field,
|
52
|
+
value=value,
|
53
|
+
form_errors=form_errors,
|
54
|
+
removable=False,
|
55
|
+
)
|
56
|
+
child = None
|
57
|
+
if value:
|
58
|
+
for typ in types:
|
59
|
+
try:
|
60
|
+
typ(**value)
|
61
|
+
except ValidationError:
|
62
|
+
pass
|
63
|
+
else:
|
64
|
+
child = self.factory.build(
|
65
|
+
typ,
|
66
|
+
name=field_name,
|
67
|
+
field=field,
|
68
|
+
value=value,
|
69
|
+
form_errors=form_errors,
|
70
|
+
removable=False,
|
71
|
+
)
|
72
|
+
|
73
|
+
widget = UnionWidget(
|
74
|
+
field_name,
|
75
|
+
# we assume those types are BaseModel
|
76
|
+
value=child,
|
77
|
+
children_types=types, # type: ignore
|
78
|
+
title=field.title if field else "",
|
79
|
+
hint=field.description if field else None,
|
80
|
+
aria_label=(
|
81
|
+
field.json_schema_extra.get("aria_label") # type:ignore
|
82
|
+
if field and field.json_schema_extra
|
83
|
+
else None
|
84
|
+
),
|
85
|
+
token=self.factory.token,
|
86
|
+
removable=removable,
|
87
|
+
error=form_errors.get(field_name),
|
88
|
+
)
|
89
|
+
|
90
|
+
return widget
|
@@ -1,7 +1,9 @@
|
|
1
1
|
"""Widget base class."""
|
2
|
+
|
2
3
|
import abc
|
3
4
|
import secrets
|
4
|
-
from
|
5
|
+
from collections.abc import Mapping
|
6
|
+
from typing import Any, Generic, TypeVar
|
5
7
|
|
6
8
|
from markupsafe import Markup
|
7
9
|
|
@@ -11,7 +13,7 @@ from fastlife.shared_utils.infer import is_union
|
|
11
13
|
T = TypeVar("T")
|
12
14
|
|
13
15
|
|
14
|
-
def get_title(typ:
|
16
|
+
def get_title(typ: type[Any]) -> str:
|
15
17
|
return getattr(
|
16
18
|
getattr(typ, "__meta__", None),
|
17
19
|
"title",
|
@@ -81,7 +83,7 @@ class Widget(abc.ABC, Generic[T]):
|
|
81
83
|
return Markup(renderer.render_template(self.get_template(), widget=self))
|
82
84
|
|
83
85
|
|
84
|
-
def _get_fullname(typ:
|
86
|
+
def _get_fullname(typ: type[Any]) -> str:
|
85
87
|
if is_union(typ):
|
86
88
|
typs = [_get_fullname(t) for t in typ.__args__] # type: ignore
|
87
89
|
return "|".join(typs) # type: ignore
|
@@ -102,7 +104,7 @@ class TypeWrapper:
|
|
102
104
|
|
103
105
|
def __init__(
|
104
106
|
self,
|
105
|
-
typ:
|
107
|
+
typ: type[Any],
|
106
108
|
route_prefix: str,
|
107
109
|
name: str,
|
108
110
|
token: str,
|
@@ -2,7 +2,7 @@
|
|
2
2
|
Widget for field of type Enum or Literal.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from
|
5
|
+
from collections.abc import Sequence
|
6
6
|
|
7
7
|
from .base import Widget
|
8
8
|
|
@@ -26,14 +26,14 @@ class DropDownWidget(Widget[str]):
|
|
26
26
|
self,
|
27
27
|
name: str,
|
28
28
|
*,
|
29
|
-
title:
|
30
|
-
hint:
|
31
|
-
aria_label:
|
32
|
-
value:
|
29
|
+
title: str | None,
|
30
|
+
hint: str | None = None,
|
31
|
+
aria_label: str | None = None,
|
32
|
+
value: str | None = None,
|
33
33
|
error: str | None = None,
|
34
|
-
options: Sequence[
|
34
|
+
options: Sequence[tuple[str, str]] | Sequence[str],
|
35
35
|
removable: bool = False,
|
36
|
-
token:
|
36
|
+
token: str | None = None,
|
37
37
|
) -> None:
|
38
38
|
super().__init__(
|
39
39
|
name,
|
@@ -1,4 +1,5 @@
|
|
1
|
-
from
|
1
|
+
from collections.abc import Sequence
|
2
|
+
from typing import Any
|
2
3
|
|
3
4
|
from markupsafe import Markup
|
4
5
|
|
@@ -17,7 +18,7 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
|
|
17
18
|
aria_label: str | None = None,
|
18
19
|
value: Sequence[Widget[Any]] | None,
|
19
20
|
error: str | None = None,
|
20
|
-
item_type:
|
21
|
+
item_type: type[Any],
|
21
22
|
token: str,
|
22
23
|
removable: bool,
|
23
24
|
):
|
@@ -1,5 +1,4 @@
|
|
1
1
|
from collections.abc import Sequence
|
2
|
-
from typing import Optional
|
3
2
|
|
4
3
|
from .base import Widget
|
5
4
|
|
@@ -23,10 +22,10 @@ class TextWidget(Widget[str]):
|
|
23
22
|
self,
|
24
23
|
name: str,
|
25
24
|
*,
|
26
|
-
title:
|
27
|
-
hint:
|
28
|
-
aria_label:
|
29
|
-
placeholder:
|
25
|
+
title: str | None,
|
26
|
+
hint: str | None = None,
|
27
|
+
aria_label: str | None = None,
|
28
|
+
placeholder: str | None = None,
|
30
29
|
error: str | None = None,
|
31
30
|
value: str = "",
|
32
31
|
input_type: str = "text",
|
@@ -87,12 +86,12 @@ class TextareaWidget(Widget[Sequence[str]]):
|
|
87
86
|
self,
|
88
87
|
name: str,
|
89
88
|
*,
|
90
|
-
title:
|
91
|
-
hint:
|
92
|
-
aria_label:
|
93
|
-
placeholder:
|
89
|
+
title: str | None,
|
90
|
+
hint: str | None = None,
|
91
|
+
aria_label: str | None = None,
|
92
|
+
placeholder: str | None = None,
|
94
93
|
error: str | None = None,
|
95
|
-
value:
|
94
|
+
value: Sequence[str] | None = None,
|
96
95
|
removable: bool = False,
|
97
96
|
token: str,
|
98
97
|
) -> None:
|
@@ -1,7 +1,9 @@
|
|
1
1
|
"""
|
2
2
|
Widget for field of type Union.
|
3
3
|
"""
|
4
|
-
|
4
|
+
|
5
|
+
from collections.abc import Sequence
|
6
|
+
from typing import Any, Union
|
5
7
|
|
6
8
|
from markupsafe import Markup
|
7
9
|
from pydantic import BaseModel
|
@@ -31,12 +33,12 @@ class UnionWidget(Widget[Widget[Any]]):
|
|
31
33
|
self,
|
32
34
|
name: str,
|
33
35
|
*,
|
34
|
-
title:
|
35
|
-
hint:
|
36
|
-
aria_label:
|
37
|
-
value:
|
36
|
+
title: str | None,
|
37
|
+
hint: str | None = None,
|
38
|
+
aria_label: str | None = None,
|
39
|
+
value: Widget[Any] | None,
|
38
40
|
error: str | None = None,
|
39
|
-
children_types: Sequence[
|
41
|
+
children_types: Sequence[type[BaseModel]],
|
40
42
|
removable: bool = False,
|
41
43
|
token: str,
|
42
44
|
):
|
@@ -72,7 +74,7 @@ class UnionWidget(Widget[Widget[Any]]):
|
|
72
74
|
widget=self,
|
73
75
|
types=self.build_types(renderer.route_prefix),
|
74
76
|
parent_type=TypeWrapper(
|
75
|
-
Union[tuple(self.children_types)], # type: ignore
|
77
|
+
Union[tuple(self.children_types)], # type: ignore # noqa: UP007
|
76
78
|
renderer.route_prefix,
|
77
79
|
self.parent_name,
|
78
80
|
self.token,
|
fastlife/components/Form.jinja
CHANGED
@@ -22,6 +22,15 @@
|
|
22
22
|
] = None,
|
23
23
|
method: Annotated[Literal["get", "post"] | None, "Http method used"] = None,
|
24
24
|
action: Annotated[str | None, "url where the form will be submitted"] = None,
|
25
|
+
hx_target: Annotated[
|
26
|
+
str | None,
|
27
|
+
"target the element for swapping than the one issuing the AJAX request."
|
28
|
+
] = None,
|
29
|
+
hx_select: Annotated[str | None, "select the content swapped from response of the AJAX request."] = None,
|
30
|
+
hx_swap: Annotated[
|
31
|
+
str | None,
|
32
|
+
"specify how the response will be swapped in relative to the target of an AJAX request."
|
33
|
+
] = None,
|
25
34
|
hx_post: Annotated[
|
26
35
|
str | Literal[True] | None,
|
27
36
|
"url where the form will be submitted using htmx. if ``True``, the current url is used."\
|
@@ -40,6 +49,9 @@
|
|
40
49
|
{%- if hx_post is not none %}
|
41
50
|
hx-post="{% if hx_post is not true %}{{hx_post}}{% endif %}"
|
42
51
|
{%- endif %}
|
52
|
+
{%- if hx_select %} hx-select="{{ hx_select }}" {%- endif %}
|
53
|
+
{%- if hx_swap %} hx-swap="{{ hx_swap }}" {%- endif %}
|
54
|
+
{%- if hx_target %} hx-target="{{ hx_target }}" {%- endif %}
|
43
55
|
{%- endif %}
|
44
56
|
{%- if action is not none %} action="{{action}}" {%- endif %}
|
45
57
|
{%- if method %} method="{{method}}" {%- endif -%}
|
fastlife/config/configurator.py
CHANGED
@@ -11,32 +11,29 @@ The configurator is designed to handle the setup during the configuration
|
|
11
11
|
phase.
|
12
12
|
"""
|
13
13
|
|
14
|
-
import importlib
|
15
|
-
import inspect
|
16
14
|
import logging
|
17
15
|
from collections import defaultdict
|
18
|
-
from collections.abc import Mapping, Sequence
|
16
|
+
from collections.abc import Callable, Mapping, Sequence
|
19
17
|
from enum import Enum
|
20
18
|
from pathlib import Path
|
21
19
|
from types import ModuleType
|
22
|
-
from typing import TYPE_CHECKING, Annotated, Any,
|
20
|
+
from typing import TYPE_CHECKING, Annotated, Any, Generic, Self, TypeVar
|
23
21
|
|
24
22
|
import venusian
|
25
|
-
from fastapi import Depends, FastAPI
|
23
|
+
from fastapi import Depends, FastAPI, Response
|
26
24
|
from fastapi import Request as BaseRequest
|
27
|
-
from fastapi import Response
|
28
25
|
from fastapi.params import Depends as DependsType
|
29
26
|
from fastapi.staticfiles import StaticFiles
|
30
27
|
from fastapi.types import IncEx
|
31
28
|
|
32
29
|
from fastlife.config.openapiextra import OpenApiTag
|
33
30
|
from fastlife.middlewares.base import AbstractMiddleware
|
34
|
-
from fastlife.request.request import Request
|
31
|
+
from fastlife.request.request import GenericRequest, Request
|
35
32
|
from fastlife.routing.route import Route
|
36
33
|
from fastlife.routing.router import Router
|
37
34
|
from fastlife.security.csrf import check_csrf
|
38
35
|
from fastlife.services.policy import check_permission
|
39
|
-
from fastlife.shared_utils.resolver import resolve
|
36
|
+
from fastlife.shared_utils.resolver import resolve, resolve_maybe_relative
|
40
37
|
|
41
38
|
from .registry import DefaultRegistry, TRegistry
|
42
39
|
from .settings import Settings
|
@@ -127,9 +124,9 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
127
124
|
self.registry = registry_cls(settings)
|
128
125
|
Route._registry = self.registry # type: ignore
|
129
126
|
|
130
|
-
self.middlewares: list[
|
131
|
-
self.exception_handlers: list[
|
132
|
-
self.mounts: list[
|
127
|
+
self.middlewares: list[tuple[type[AbstractMiddleware], Any]] = []
|
128
|
+
self.exception_handlers: list[tuple[int | type[Exception], Any]] = []
|
129
|
+
self.mounts: list[tuple[str, Path, str]] = []
|
133
130
|
self.tags: dict[str, OpenApiTag] = {}
|
134
131
|
|
135
132
|
self.api_title = "OpenAPI"
|
@@ -142,7 +139,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
142
139
|
self._route_prefix: str = ""
|
143
140
|
self._routers: dict[str, Router] = defaultdict(Router)
|
144
141
|
self._security_policies: dict[
|
145
|
-
str,
|
142
|
+
str, type[AbstractSecurityPolicy[Any, TRegistry]]
|
146
143
|
] = {}
|
147
144
|
|
148
145
|
self._registered_permissions: set[str] = set()
|
@@ -239,12 +236,11 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
239
236
|
:param ignore: ignore submodules
|
240
237
|
"""
|
241
238
|
if isinstance(module, str):
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
239
|
+
try:
|
240
|
+
module = resolve_maybe_relative(module, stack_depth=2)
|
241
|
+
except ModuleNotFoundError as exc:
|
242
|
+
raise ConfigurationError(f"Can't resolve {module}") from exc
|
246
243
|
|
247
|
-
module = importlib.import_module(module, package)
|
248
244
|
old, self._route_prefix = self._route_prefix, route_prefix
|
249
245
|
try:
|
250
246
|
self.scanner.scan( # type: ignore
|
@@ -302,7 +298,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
302
298
|
self.api_redoc_url = redoc_url
|
303
299
|
return self
|
304
300
|
|
305
|
-
def
|
301
|
+
def add_openapi_tag(self, tag: OpenApiTag) -> Self:
|
306
302
|
"""Register a tag description in the documentation."""
|
307
303
|
if tag.name in self.tags:
|
308
304
|
raise ConfigurationError(f"Tag {tag.name} can't be registered twice.")
|
@@ -310,7 +306,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
310
306
|
return self
|
311
307
|
|
312
308
|
def add_middleware(
|
313
|
-
self, middleware_class:
|
309
|
+
self, middleware_class: type[AbstractMiddleware], **options: Any
|
314
310
|
) -> Self:
|
315
311
|
"""
|
316
312
|
Add a starlette middleware to the FastAPI app.
|
@@ -522,7 +518,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
522
518
|
|
523
519
|
def add_exception_handler(
|
524
520
|
self,
|
525
|
-
status_code_or_exc: int |
|
521
|
+
status_code_or_exc: int | type[Exception],
|
526
522
|
handler: Any,
|
527
523
|
*,
|
528
524
|
template: str | None = None,
|
@@ -539,8 +535,8 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
539
535
|
# class is wrong.
|
540
536
|
# Until we store a security policy per rooter, we rebuild an
|
541
537
|
# incomplete request here.
|
542
|
-
|
543
|
-
resp = handler(
|
538
|
+
req = GenericRequest[DefaultRegistry](self.registry, request)
|
539
|
+
resp = handler(req, exc)
|
544
540
|
if isinstance(resp, Response):
|
545
541
|
return resp
|
546
542
|
|
@@ -552,7 +548,7 @@ class GenericConfigurator(Generic[TRegistry]):
|
|
552
548
|
"did not return a Response"
|
553
549
|
)
|
554
550
|
|
555
|
-
return
|
551
|
+
return req.registry.get_renderer(template)(req).render(
|
556
552
|
template,
|
557
553
|
params=resp,
|
558
554
|
status_code=status_code,
|
@@ -594,8 +590,11 @@ class Configurator(GenericConfigurator[DefaultRegistry]):
|
|
594
590
|
"""
|
595
591
|
|
596
592
|
|
593
|
+
TConfigurator = TypeVar("TConfigurator", bound=GenericConfigurator[Any])
|
594
|
+
|
595
|
+
|
597
596
|
def configure(
|
598
|
-
wrapped: Callable[[
|
597
|
+
wrapped: Callable[[TConfigurator], None],
|
599
598
|
) -> Callable[[Any], None]:
|
600
599
|
"""
|
601
600
|
Decorator used to attach route in a submodule while using the configurator.include.
|
fastlife/config/exceptions.py
CHANGED